<filename>microscopemetrics/devices/devices.py<gh_stars>0
from enum import Enum, EnumMeta
from configparser import NoOptionError
from math import sin, asin, cos


# from microscopemetrics.interface import omero as interface

# Creating logging services
import logging
module_logger = logging.getLogger('metrics.devices.devices')


DTYPES = {
    'int': (tuple,),
    'float': (tuple,),
    'bool': (type(None),),
    'str': (int,),
    'tuple': (type(None),),
}

def _call_if_callable(f):
    """Call callables, or return value of non-callables."""
    return f() if callable(f) else f


class _Setting:
    """The code of this class was copied from https://github.com/MicronOxford/microscope
    """
    # Settings classes should be private: devices should use a factory method
    # rather than instantiate settings directly; most already use add_setting
    # for this.
    # TODO: Implement a get units
    def __init__(self, name, dtype, get_from_db_func, get_from_conf_func, get_from_name_func, set_func, values=None):
        """Create a setting.
        :param name: the setting's name
        :param dtype: a data type from ('int', 'float', 'bool', 'enum', 'str')
        :param get_from_db_func: a function to get the value from the database interface
        :param get_from_conf_func: a function to get the value from the device configuration
        :param get_from_name_func: a function to get the value from the image name
        :param values: a description of allowed values dependent on dtype,
                       or function that returns a description
        Setters and getters accept or return:
            the setting value for int, float, bool and str;
            the setting index into a list, dict or Enum type for enum.
        """
        self.name = name
        if dtype not in DTYPES:
            raise Exception('Unsupported dtype.')
        elif not (isinstance(values, DTYPES[dtype]) or callable(values)):
            raise Exception("Invalid values type for %s '%s':"
                            " expected function or %s"
                            % (dtype, name, DTYPES[dtype]))
        self.dtype = dtype
        self._get_from_db = get_from_db_func
        self._get_from_conf = get_from_conf_func
        self._get_from_name = get_from_name_func
        self._values = values

    def describe(self):
        """Returns a dictionary describing the setting
        """
        return {'type': self.dtype,
                'values': self.values()}

    def get(self, **kwargs):
        """Get a setting"""
        if self._get_from_db is not None:
            value = self._get_from_db(**kwargs)
            if value is not None:
                return value
        if self._get_from_conf is not None:
            value = self._get_from_conf(**kwargs)
            if value is not None:
                return value
        if self._get_from_name is not None:
            value = self._get_from_name(**kwargs)
            if value is not None:
                return value
        module_logger.warning(f'Device setting could not be retrieved: {self.name}')

    def values(self):
        values = _call_if_callable(self._values)
        if values is not None:
            if self.dtype == 'enum':
                if isinstance(values, dict):
                    return list(values.items())
                else:
                    # self._values is a list or tuple
                    return list(enumerate(values))
            elif self._values is not None:
                return values


class Device:
    """A superclass for all the microscope devices and eventually other type of devices."""
    def __init__(self, device_config):
        self.device_config = device_config
        self._settings = {}

    def add_setting(self, name, dtype, get_from_db_func, get_from_conf_func, get_from_name_func, set_func, values=None):
        """Add a setting definition.
        :param name: the setting's name
        :param dtype: a data type from ('int', 'float', 'bool', 'enum', 'str')
        :param get_from_db_func: a function to get the value from the interface to the database
        :param get_from_conf_func: a function to get the value from the configuration
        :param get_from_name_func: a function to get the value from the image name
        :param set_func: a function to set the value
        :param values: a description of allowed values dependent on dtype,
                       or function that returns a description
        A client needs some way of knowing a setting name and data type,
        retrieving the current value and, if settable, a way to retrieve
        allowable values, and set the value.
        """
        if dtype not in DTYPES:
            raise Exception('Unsupported dtype.')
        elif not (isinstance(values, DTYPES[dtype]) or callable(values)):
            raise Exception("Invalid values type for %s '%s':"
                            " expected function or %s"
                            % (dtype, name, DTYPES[dtype]))
        else:
            self._settings[name] = _Setting(name, dtype,
                                            get_from_db_func,
                                            get_from_conf_func,
                                            get_from_name_func,
                                            set_func,
                                            values)

    def get_setting(self, name, **kwargs):
        """Tries to get a specified setting from the following sources by order of preference:
        - Image from the database interface
        - The microscope analysis_config linked through the image name
        - Directly in the image name"""

        try:
            return self._settings[name].get(**kwargs)
        except Exception as err:
            module_logger.error("in get_setting(%s):", name, exc_info=err)
            raise

    def get_all_settings(self, **kwargs):
        """Return settings as a dict."""
        def catch(f):
            try:
                return f(**kwargs)
            except Exception as err:
                module_logger.error(f'getting {f.__self__.name}: {err}')
                return None
        return {k: catch(v.get) for k, v in self._settings.items()}

    def set_setting(self, name, value):
        """Set a setting."""
        try:
            self._settings[name].set(value)
        except Exception as err:
            module_logger.error("in set_setting(%s):", name, exc_info=err)
            raise

    def describe_setting(self, name):
        """Return ordered setting descriptions as a list of dicts."""
        return self._settings[name].describe()

    def describe_settings(self):
        """Return ordered setting descriptions as a list of dicts."""
        return [(k, v.describe()) for (k, v) in self._settings.items()]

    def settings_to_conf(self):
        pass

    def update_settings(self, incoming, init=False):
        """Update settings based on dict of settings and values."""
        if init:
            # Assume nothing about state: set everything.
            my_keys = set(self._settings.keys())
            their_keys = set(incoming.keys())
            update_keys = my_keys & their_keys
            if update_keys != my_keys:
                missing = ', '.join([k for k in my_keys - their_keys])
                msg = 'update_settings init=True but missing keys: %s.' % missing
                _logger.debug(msg)
                raise Exception(msg)
        else:
            # Only update changed values.
            my_keys = set(self._settings.keys())
            their_keys = set(incoming.keys())
            update_keys = set(key for key in my_keys & their_keys
                              if self.get_setting(key) != incoming[key])
        results = {}
        # Update values.
        for key in update_keys:
            if key not in my_keys or not self._settings[key].set:
                # Setting not recognised or no set function implemented
                results[key] = NotImplemented
                update_keys.remove(key)
                continue
            if _call_if_callable(self._settings[key].readonly):
                continue
            self._settings[key].set(incoming[key])
        # Read back values in second loop.
        for key in update_keys:
            results[key] = self._settings[key].get()
        return results


class Microscope(Device):
    """A superclass for the microscopes. Inherit this class when you create a new type of microscope."""
    def __init__(self, device_config):
        super().__init__(device_config)

    def _get_conf_objective_nr(self, image):
        img_name = image.getName()
        obj_nrs = [i for i, token in enumerate(self.device_config.getlist('OBJECTIVES', 'names')) if token in img_name]
        if len(obj_nrs) > 1:
            module_logger.error('More than one reference to an objective lens was found in the image name. Only the first one will be considered.')
        elif len(obj_nrs) == 0:
            module_logger.info('No references to any objective lens were found in the image name')
            return None
        return obj_nrs[0]

    def _get_conf_objective_setting(self, option, image):
        obj_nr = self._get_conf_objective_nr(image)
        if obj_nr is None:
            return None
        try:
            values = eval(self.device_config.get('OBJECTIVES', option))
        except NoOptionError as e:
            module_logger.error(f'No parameters for {option} have been defined in the device configuration')

        except NameError as e:
            module_logger.error(f'There was an error reading microscope objectives configuration option: {option}')
            return None
        if values is None:
            module_logger.info(f'No information available for {option} in the microscope objectives configuration ')
            return None
        else:
            return values[obj_nr]

    def _get_conf_channel_nrs(self, image):
        img_name = image.getName()
        ch_nrs = list()
        token_positions = list()
        for ch, ch_token in enumerate(self.device_config.getlist('CHANNELS', 'names')):
            token_pos = img_name.find(ch_token)
            if token_pos == -1:  # token not in name
                continue
            else:
                token_positions.append(token_pos)
                ch_nrs.append(ch)
        # Sort channels according to their position in the name
        ch_nrs = tuple(ch for _, ch in sorted(zip(token_positions, ch_nrs)))
        if len(ch_nrs) == 0:
            module_logger.info('No references to any channel were found in the image name')
            return None
        return ch_nrs

    def _get_conf_channel_settings(self, option, image):
        ch_nrs = self._get_conf_channel_nrs(image)
        if ch_nrs is None:
            return None
        try:
            values = eval(self.device_config.get('CHANNELS', option))
        except NameError as e:
            module_logger.error(f'There was an error reading microscope channel configuration option: {option}')
            return None
        if values is None:
            module_logger.info(f'No information available for {option} in the microscope channels configuration ')
            return None
        else:
            return tuple(values[ch] for ch in ch_nrs)

    def _get_metadata_from_name(self, token_left, token_right, metadata_type, image):
        name = image.getName()
        start = name.find(token_left)
        if start == -1:
            return None
        name = name[start + len(token_left):]
        end = name.find(token_right)
        value = metadata_type(name[:end])
        if metadata_type is None:
            return value
        else:
            try:
                value = metadata_type(value)
            except ValueError as e:
                module_logger.error(f'Could not cast {value} into {metadata_type}. Please verify file naming for token {token_left}')
                return None

        return value


class WideFieldMicroscope(Microscope):
    """A Widefield microscope"""
    def __init__(self, device_config):
        super().__init__(device_config)

        # Setting some objective lens settings
        self.add_setting('objective_lens_refractive_index', 'float',
                         # get_from_db_func=interface._get_objective_lens_refractive_index,
                         get_from_db_func=self._get_objective_lens_refractive_index,
                         get_from_conf_func=self._get_conf_objective_lens_refr_index,
                         get_from_name_func=self._get_name_objective_lens_refr_index,
                         set_func=None,
                         values=(1.0, 2.0))
        self.add_setting('objective_lens_na', 'float',
                         # get_from_db_func=interface._get_objective_lens_na,
                         get_from_db_func=self._get_objective_lens_na,
                         get_from_conf_func=self._get_conf_objective_lens_na,
                         get_from_name_func=self._get_name_objective_lens_na,
                         set_func=None,
                         values=(0.0, 2.0))
        self.add_setting('objective_lens_nominal_magnification', 'float',
                         # get_from_db_func=interface._get_objective_lens_nominal_magnification,
                         get_from_db_func=self._get_objective_lens_nominal_magnification,
                         get_from_conf_func=self._get_conf_objective_lens_nominal_magnification,
                         get_from_name_func=self._get_name_objective_lens_nominal_magnification,
                         set_func=None,
                         values=(1.0, 1000.0))

        # Setting some channel settings
        self.add_setting('excitation_wavelengths', 'tuple',
                         # get_from_db_func=interface._get_excitation_wavelengths,
                         get_from_db_func=self._get_excitation_wavelengths,
                         get_from_conf_func=self._get_conf_excitation_wavelengths,
                         get_from_name_func=self._get_name_excitation_wavelengths,
                         set_func=None,
                         values=None)
        self.add_setting('emission_wavelengths', 'tuple',
                         # get_from_db_func=interface._get_emission_wavelengths,
                         get_from_db_func=self._get_emission_wavelengths,
                         get_from_conf_func=self._get_conf_emission_wavelengths,
                         get_from_name_func=self._get_name_emission_wavelengths,
                         set_func=None,
                         values=None)

    def get_all_settings(self, **kwargs):
        settings = super().get_all_settings(**kwargs)
        settings.update(self.get_theoretical_res(**kwargs))
        settings.update(self.get_nyquist(**kwargs))

        return settings

    def _get_conf_objective_lens_refr_index(self, image):
        return self._get_conf_objective_setting('objective_lens_refractive_index', image)

    def _get_name_objective_lens_refr_index(self, image):
        return self._get_metadata_from_name('_RI=', '_', float, image)

    def _get_conf_objective_lens_na(self, image):
        return self._get_conf_objective_setting('objective_lens_na', image)

    def _get_name_objective_lens_na(self, image):
        return self._get_metadata_from_name('_NA=', '_', float, image)

    def _get_conf_objective_lens_nominal_magnification(self, image):
        return self._get_conf_objective_setting('objective_lens_nominal_magnification', image)

    def _get_name_objective_lens_nominal_magnification(self, image):
        return self._get_metadata_from_name('_MAG=', '_', float, image)

    def _get_conf_excitation_wavelengths(self, image):
        return self._get_conf_channel_settings('excitation_wavelengths', image)

    def _get_name_excitation_wavelengths(self, image):
        return self._get_metadata_from_name('_EX=', '_', list, image)

    def _get_conf_emission_wavelengths(self, image):
        return self._get_conf_channel_settings('emission_wavelengths', image)

    def _get_name_emission_wavelengths(self, image):
        return self._get_metadata_from_name('_EM=', '_', list, image)

    # TODO: to move to interface
    def _get_objective_lens_refractive_index(self, image):
        objective_settings = image.getObjectiveSettings()
        if objective_settings is None:
            return None
        return objective_settings.getRefractiveIndex()

    def _get_objective_lens_na(self, image):
        objective_settings = image.getObjectiveSettings()
        if objective_settings is None:
            return None
        objective = objective_settings.getObjective()
        if objective is None:
            return None
        return objective.getLensNA()

    def _get_objective_lens_nominal_magnification(self, image):
        objective_settings = image.getObjectiveSettings()
        if objective_settings is None:
            return None
        objective = objective_settings.getObjective()
        if objective is None:
            return None
        return objective.getNominalMagnification()

    def _get_excitation_wavelengths(self, image):
        channels = image.getChannels()
        if channels is None:
            return None
        wavelengths = tuple(ch.getExcitationWave() for ch in channels)
        if not all(wavelengths):
            return None
        else:
            return wavelengths

    def _get_emission_wavelengths(self, image):
        channels = image.getChannels()
        if channels is None:
            return None
        wavelengths = tuple(ch.getEmissionWave() for ch in channels)
        if not all(wavelengths):
            return None
        else:
            return wavelengths

    def get_theoretical_res_fwhm(self, **kwargs):
        """Returns a dictionary containing the lateral and axial FWHM theoretical resolutions for every channel in image.

        :param image: an image object
        :return: a dictionary in the form:
                 {'resolution_theoretical_fwhm_lateral': list of lateral resolutions for every channel,
                  'resolution_theoretical_fwhm_axial': list of axial resolutions for every channel,
                  'resolution_theoretical_fwhm_units': string specifying units}
        """
        theoretical_res = {'resolution_theoretical_fwhm_lateral': [],
                           'resolution_theoretical_fwhm_axial': [],
                           'resolution_theoretical_fwhm_units': 'NANOMETER'}
        na = self.get_setting('objective_lens_na', **kwargs)
        for em in self.get_setting('emission_wavelengths', **kwargs):
            try:
                theoretical_res['resolution_theoretical_fwhm_lateral'].append(.353 * em / na)
            except TypeError as e:
                module_logger.warning('FWHM theoretical resolution could not be calculated. Verify configuration files.')
                theoretical_res['resolution_theoretical_fwhm_lateral'].append(None)
            try:
                theoretical_res['resolution_theoretical_fwhm_axial'].append(None)  # TODO: find formula for fwhm axial resolution
            except TypeError as e:
                module_logger.warning('FWHM theoretical resolution could not be calculated. Verify configuration files.')
                theoretical_res['resolution_theoretical_fwhm_axial'].append(None)

        return theoretical_res

    def get_theoretical_res_rayleigh(self, **kwargs):
        """Returns a dictionary containing the lateral and axial Rayleigh theoretical resolutions for every channel in image.

        :param image: an image object
        :return: a dictionary in the form:
                 {'resolution_theoretical_rayleigh_lateral': list of lateral resolutions for every channel,
                  'resolution_theoretical_rayleigh_axial': list of axial resolutions for every channel,
                  'resolution_theoretical_rayleigh_units': string specifying units}
        """
        theoretical_res = {'resolution_theoretical_rayleigh_lateral': [],
                           'resolution_theoretical_rayleigh_axial': [],
                           'resolution_theoretical_rayleigh_units': 'NANOMETER'}
        na = self.get_setting('objective_lens_na', **kwargs)
        ri = self.get_setting('objective_lens_refractive_index', **kwargs)
        for em in self.get_setting('emission_wavelengths', **kwargs):
            try:
                theoretical_res['resolution_theoretical_rayleigh_lateral'].append(.61 * em / na)
            except TypeError as e:
                module_logger.warning('Rayleigh theoretical lateral resolution could not be calculated. Verify configuration files.')
                theoretical_res['resolution_theoretical_rayleigh_lateral'].append(None)
            try:
                theoretical_res['resolution_theoretical_rayleigh_axial'].append(2 * em * ri / na ** 2)
            except TypeError as e:
                module_logger.warning('Rayleigh theoretical axial resolution could not be calculated. Verify configuration files.')
                theoretical_res['resolution_theoretical_rayleigh_axial'].append(None)

        return theoretical_res

    def get_theoretical_res(self, **kwargs):
        """Returns a dictionary containing the theoretical resolutions for every channel in image in all the
        implemented methods."""
        theoretical_res = dict()
        theoretical_res.update(self.get_theoretical_res_fwhm(**kwargs))
        theoretical_res.update(self.get_theoretical_res_rayleigh(**kwargs))

        return theoretical_res

    def get_nyquist(self, **kwargs):
        """Returns a dictionary containing the nyquist sampling criteria values for every channel in image.
        """
        nyquist_delta = {'nyquist_lateral': [],
                         'nyquist_axial': [],
                         'nyquist_units': 'NANOMETER'}  # TODO: FIX units for nyquist and resolution. Get them from wavelength units.
        na = self.get_setting('objective_lens_na', **kwargs)
        ri = self.get_setting('objective_lens_refractive_index', **kwargs)
        for em in self.get_setting('emission_wavelengths', **kwargs):
            try:
                nyquist_delta['nyquist_lateral'].append(em / (4 * ri * (na / ri)))
            except TypeError as e:
                module_logger.warning('Lateral Nyquist criterion could not be calculated. Verify configuration files.')
                nyquist_delta['nyquist_lateral'].append(None)
            try:
                nyquist_delta['nyquist_axial'].append(em / (2 * ri * (1 - cos(asin(na / ri)))))
            except TypeError as e:
                module_logger.warning('Axial Nyquist criterion could not be calculated. Verify configuration files.')
                nyquist_delta['nyquist_axial'].append(None)

        return nyquist_delta
        #
        # def calculate_theoretcal_resolution(microscope_type, na, refractive_index, emission_wave, excitation_wave=None):
        #     theoretical_res = {}
        #     if microscope_type is None:
        #         module_logger.warning(
        #             'Microscope type undefined to calculate theoretical resolution. Falling back into Wide-Field')
        #         theoretical_res = calculate_theoretcal_resolution('wf', na, refractive_index, emission_wave,
        #                                                           excitation_wave=excitation_wave)
        #     elif microscope_type.lower() in ['wf', 'wide-field', 'widefield']:
        #         theoretical_res['fwhm_lateral'] = .353 * emission_wave / na
        #         theoretical_res['rayleigh_lateral'] = .61 * emission_wave / na
        #         theoretical_res['rayleigh_axial'] = 2 * emission_wave * refractive_index / na ** 2
        #         theoretical_res['units'] = 'NANOMETER'
        #     elif microscope_type.lower() in ['confocal']:
        #         theoretical_res['fwhm_lateral'] = .353 * emission_wave / na
        #         theoretical_res['rayleigh_lateral'] = .4 * emission_wave / na
        #         theoretical_res['rayleigh_axial'] = 1.4 * emission_wave * refractive_index / na ** 2
        #         theoretical_res['units'] = 'NANOMETER'
        #     else:
        #         module_logger.warning(
        #             'Could not find microscope type to calculate theoretical resolution. Falling back into Wide-Field')
        #         theoretical_res = calculate_theoretcal_resolution('wf', na, refractive_index, emission_wave,
        #                                                           excitation_wave=excitation_wave)
        #
        #     return theoretical_res

        #
        # if refractive_index is None:
        #     module_logger.warning('Refractive index is being guessed. Nyquist criteria will not be correct.')
        #     if na > .8:
        #         refractive_index = 1.5
        #     else:
        #         refractive_index = 1.0
        # alpha = asin(na / refractive_index)
        # # Theoretical resolutions for confocal microscope are only attained with very closed pinhole
        # # We add a tolerance factor to render theoretical resolution more realistic
        # tolerance = 1.6
        # nyquist_delta = {}
        # if microscope_type is None:
        #     module_logger.warning(
        #         'Microscope type undefined to calculate Nyquist criterion. Falling back into Wide-Field')
        #     nyquist_delta = calculate_nyquist('wf', na, refractive_index, emission_wave, excitation_wave)
        # elif microscope_type.lower() in ['wf', 'wide-field', 'widefield']:
        #     nyquist_delta['lateral'] = emission_wave / (4 * refractive_index * sin(alpha))
        #     nyquist_delta['axial'] = emission_wave / (2 * refractive_index * (1 - cos(alpha)))
        #     nyquist_delta['units'] = 'NANOMETER'
        # elif microscope_type.lower() in ['confocal']:
        #     nyquist_delta['lateral'] = tolerance * excitation_wave / (8 * refractive_index * sin(alpha))
        #     nyquist_delta['axial'] = tolerance * excitation_wave / (4 * refractive_index * (1 - cos(alpha)))
        #     nyquist_delta['units'] = 'NANOMETER'
        # else:
        #     module_logger.warning(
        #         'Could not find microscope type to calculate Nyquist criterion. Falling back into Wide-Field')
        #     nyquist_delta = calculate_nyquist('wf', na, refractive_index, emission_wave, excitation_wave)
        #
        # return nyquist_delta

<filename>microscopemetrics/devices/devices.py<gh_stars>0
from enum import Enum, EnumMeta
from configparser import NoOptionError
from math import sin, asin, cos


# from microscopemetrics.interface import omero as interface

# Creating logging services
import logging
module_logger = logging.getLogger('metrics.devices.devices')


DTYPES = {
    'int': (tuple,),
    'float': (tuple,),
    'bool': (type(None),),
    'str': (int,),
    'tuple': (type(None),),
}

def _call_if_callable(f):
    """Call callables, or return value of non-callables."""
    return f() if callable(f) else f


class _Setting:
    """The code of this class was copied from https://github.com/MicronOxford/microscope
    """
    # Settings classes should be private: devices should use a factory method
    # rather than instantiate settings directly; most already use add_setting
    # for this.
    # TODO: Implement a get units
    def __init__(self, name, dtype, get_from_db_func, get_from_conf_func, get_from_name_func, set_func, values=None):
        """Create a setting.
        :param name: the setting's name
        :param dtype: a data type from ('int', 'float', 'bool', 'enum', 'str')
        :param get_from_db_func: a function to get the value from the database interface
        :param get_from_conf_func: a function to get the value from the device configuration
        :param get_from_name_func: a function to get the value from the image name
        :param values: a description of allowed values dependent on dtype,
                       or function that returns a description
        Setters and getters accept or return:
            the setting value for int, float, bool and str;
            the setting index into a list, dict or Enum type for enum.
        """
        self.name = name
        if dtype not in DTYPES:
            raise Exception('Unsupported dtype.')
        elif not (isinstance(values, DTYPES[dtype]) or callable(values)):
            raise Exception("Invalid values type for %s '%s':"
                            " expected function or %s"
                            % (dtype, name, DTYPES[dtype]))
        self.dtype = dtype
        self._get_from_db = get_from_db_func
        self._get_from_conf = get_from_conf_func
        self._get_from_name = get_from_name_func
        self._values = values

    def describe(self):
        """Returns a dictionary describing the setting
        """
        return {'type': self.dtype,
                'values': self.values()}

    def get(self, **kwargs):
        """Get a setting"""
        if self._get_from_db is not None:
            value = self._get_from_db(**kwargs)
            if value is not None:
                return value
        if self._get_from_conf is not None:
            value = self._get_from_conf(**kwargs)
            if value is not None:
                return value
        if self._get_from_name is not None:
            value = self._get_from_name(**kwargs)
            if value is not None:
                return value
        module_logger.warning(f'Device setting could not be retrieved: {self.name}')

    def values(self):
        values = _call_if_callable(self._values)
        if values is not None:
            if self.dtype == 'enum':
                if isinstance(values, dict):
                    return list(values.items())
                else:
                    # self._values is a list or tuple
                    return list(enumerate(values))
            elif self._values is not None:
                return values


class Device:
    """A superclass for all the microscope devices and eventually other type of devices."""
    def __init__(self, device_config):
        self.device_config = device_config
        self._settings = {}

    def add_setting(self, name, dtype, get_from_db_func, get_from_conf_func, get_from_name_func, set_func, values=None):
        """Add a setting definition.
        :param name: the setting's name
        :param dtype: a data type from ('int', 'float', 'bool', 'enum', 'str')
        :param get_from_db_func: a function to get the value from the interface to the database
        :param get_from_conf_func: a function to get the value from the configuration
        :param get_from_name_func: a function to get the value from the image name
        :param set_func: a function to set the value
        :param values: a description of allowed values dependent on dtype,
                       or function that returns a description
        A client needs some way of knowing a setting name and data type,
        retrieving the current value and, if settable, a way to retrieve
        allowable values, and set the value.
        """
        if dtype not in DTYPES:
            raise Exception('Unsupported dtype.')
        elif not (isinstance(values, DTYPES[dtype]) or callable(values)):
            raise Exception("Invalid values type for %s '%s':"
                            " expected function or %s"
                            % (dtype, name, DTYPES[dtype]))
        else:
            self._settings[name] = _Setting(name, dtype,
                                            get_from_db_func,
                                            get_from_conf_func,
                                            get_from_name_func,
                                            set_func,
                                            values)

    def get_setting(self, name, **kwargs):
        """Tries to get a specified setting from the following sources by order of preference:
        - Image from the database interface
        - The microscope analysis_config linked through the image name
        - Directly in the image name"""

        try:
            return self._settings[name].get(**kwargs)
        except Exception as err:
            module_logger.error("in get_setting(%s):", name, exc_info=err)
            raise

    def get_all_settings(self, **kwargs):
        """Return settings as a dict."""
        def catch(f):
            try:
                return f(**kwargs)
            except Exception as err:
                module_logger.error(f'getting {f.__self__.name}: {err}')
                return None
        return {k: catch(v.get) for k, v in self._settings.items()}

    def set_setting(self, name, value):
        """Set a setting."""
        try:
            self._settings[name].set(value)
        except Exception as err:
            module_logger.error("in set_setting(%s):", name, exc_info=err)
            raise

    def describe_setting(self, name):
        """Return ordered setting descriptions as a list of dicts."""
        return self._settings[name].describe()

    def describe_settings(self):
        """Return ordered setting descriptions as a list of dicts."""
        return [(k, v.describe()) for (k, v) in self._settings.items()]

    def settings_to_conf(self):
        pass

    def update_settings(self, incoming, init=False):
        """Update settings based on dict of settings and values."""
        if init:
            # Assume nothing about state: set everything.
            my_keys = set(self._settings.keys())
            their_keys = set(incoming.keys())
            update_keys = my_keys & their_keys
            if update_keys != my_keys:
                missing = ', '.join([k for k in my_keys - their_keys])
                msg = 'update_settings init=True but missing keys: %s.' % missing
                _logger.debug(msg)
                raise Exception(msg)
        else:
            # Only update changed values.
            my_keys = set(self._settings.keys())
            their_keys = set(incoming.keys())
            update_keys = set(key for key in my_keys & their_keys
                              if self.get_setting(key) != incoming[key])
        results = {}
        # Update values.
        for key in update_keys:
            if key not in my_keys or not self._settings[key].set:
                # Setting not recognised or no set function implemented
                results[key] = NotImplemented
                update_keys.remove(key)
                continue
            if _call_if_callable(self._settings[key].readonly):
                continue
            self._settings[key].set(incoming[key])
        # Read back values in second loop.
        for key in update_keys:
            results[key] = self._settings[key].get()
        return results


class Microscope(Device):
    """A superclass for the microscopes. Inherit this class when you create a new type of microscope."""
    def __init__(self, device_config):
        super().__init__(device_config)

    def _get_conf_objective_nr(self, image):
        img_name = image.getName()
        obj_nrs = [i for i, token in enumerate(self.device_config.getlist('OBJECTIVES', 'names')) if token in img_name]
        if len(obj_nrs) > 1:
            module_logger.error('More than one reference to an objective lens was found in the image name. Only the first one will be considered.')
        elif len(obj_nrs) == 0:
            module_logger.info('No references to any objective lens were found in the image name')
            return None
        return obj_nrs[0]

    def _get_conf_objective_setting(self, option, image):
        obj_nr = self._get_conf_objective_nr(image)
        if obj_nr is None:
            return None
        try:
            values = eval(self.device_config.get('OBJECTIVES', option))
        except NoOptionError as e:
            module_logger.error(f'No parameters for {option} have been defined in the device configuration')

        except NameError as e:
            module_logger.error(f'There was an error reading microscope objectives configuration option: {option}')
            return None
        if values is None:
            module_logger.info(f'No information available for {option} in the microscope objectives configuration ')
            return None
        else:
            return values[obj_nr]

    def _get_conf_channel_nrs(self, image):
        img_name = image.getName()
        ch_nrs = list()
        token_positions = list()
        for ch, ch_token in enumerate(self.device_config.getlist('CHANNELS', 'names')):
            token_pos = img_name.find(ch_token)
            if token_pos == -1:  # token not in name
                continue
            else:
                token_positions.append(token_pos)
                ch_nrs.append(ch)
        # Sort channels according to their position in the name
        ch_nrs = tuple(ch for _, ch in sorted(zip(token_positions, ch_nrs)))
        if len(ch_nrs) == 0:
            module_logger.info('No references to any channel were found in the image name')
            return None
        return ch_nrs

    def _get_conf_channel_settings(self, option, image):
        ch_nrs = self._get_conf_channel_nrs(image)
        if ch_nrs is None:
            return None
        try:
            values = eval(self.device_config.get('CHANNELS', option))
        except NameError as e:
            module_logger.error(f'There was an error reading microscope channel configuration option: {option}')
            return None
        if values is None:
            module_logger.info(f'No information available for {option} in the microscope channels configuration ')
            return None
        else:
            return tuple(values[ch] for ch in ch_nrs)

    def _get_metadata_from_name(self, token_left, token_right, metadata_type, image):
        name = image.getName()
        start = name.find(token_left)
        if start == -1:
            return None
        name = name[start + len(token_left):]
        end = name.find(token_right)
        value = metadata_type(name[:end])
        if metadata_type is None:
            return value
        else:
            try:
                value = metadata_type(value)
            except ValueError as e:
                module_logger.error(f'Could not cast {value} into {metadata_type}. Please verify file naming for token {token_left}')
                return None

        return value


class WideFieldMicroscope(Microscope):
    """A Widefield microscope"""
    def __init__(self, device_config):
        super().__init__(device_config)

        # Setting some objective lens settings
        self.add_setting('objective_lens_refractive_index', 'float',
                         # get_from_db_func=interface._get_objective_lens_refractive_index,
                         get_from_db_func=self._get_objective_lens_refractive_index,
                         get_from_conf_func=self._get_conf_objective_lens_refr_index,
                         get_from_name_func=self._get_name_objective_lens_refr_index,
                         set_func=None,
                         values=(1.0, 2.0))
        self.add_setting('objective_lens_na', 'float',
                         # get_from_db_func=interface._get_objective_lens_na,
                         get_from_db_func=self._get_objective_lens_na,
                         get_from_conf_func=self._get_conf_objective_lens_na,
                         get_from_name_func=self._get_name_objective_lens_na,
                         set_func=None,
                         values=(0.0, 2.0))
        self.add_setting('objective_lens_nominal_magnification', 'float',
                         # get_from_db_func=interface._get_objective_lens_nominal_magnification,
                         get_from_db_func=self._get_objective_lens_nominal_magnification,
                         get_from_conf_func=self._get_conf_objective_lens_nominal_magnification,
                         get_from_name_func=self._get_name_objective_lens_nominal_magnification,
                         set_func=None,
                         values=(1.0, 1000.0))

        # Setting some channel settings
        self.add_setting('excitation_wavelengths', 'tuple',
                         # get_from_db_func=interface._get_excitation_wavelengths,
                         get_from_db_func=self._get_excitation_wavelengths,
                         get_from_conf_func=self._get_conf_excitation_wavelengths,
                         get_from_name_func=self._get_name_excitation_wavelengths,
                         set_func=None,
                         values=None)
        self.add_setting('emission_wavelengths', 'tuple',
                         # get_from_db_func=interface._get_emission_wavelengths,
                         get_from_db_func=self._get_emission_wavelengths,
                         get_from_conf_func=self._get_conf_emission_wavelengths,
                         get_from_name_func=self._get_name_emission_wavelengths,
                         set_func=None,
                         values=None)

    def get_all_settings(self, **kwargs):
        settings = super().get_all_settings(**kwargs)
        settings.update(self.get_theoretical_res(**kwargs))
        settings.update(self.get_nyquist(**kwargs))

        return settings

    def _get_conf_objective_lens_refr_index(self, image):
        return self._get_conf_objective_setting('objective_lens_refractive_index', image)

    def _get_name_objective_lens_refr_index(self, image):
        return self._get_metadata_from_name('_RI=', '_', float, image)

    def _get_conf_objective_lens_na(self, image):
        return self._get_conf_objective_setting('objective_lens_na', image)

    def _get_name_objective_lens_na(self, image):
        return self._get_metadata_from_name('_NA=', '_', float, image)

    def _get_conf_objective_lens_nominal_magnification(self, image):
        return self._get_conf_objective_setting('objective_lens_nominal_magnification', image)

    def _get_name_objective_lens_nominal_magnification(self, image):
        return self._get_metadata_from_name('_MAG=', '_', float, image)

    def _get_conf_excitation_wavelengths(self, image):
        return self._get_conf_channel_settings('excitation_wavelengths', image)

    def _get_name_excitation_wavelengths(self, image):
        return self._get_metadata_from_name('_EX=', '_', list, image)

    def _get_conf_emission_wavelengths(self, image):
        return self._get_conf_channel_settings('emission_wavelengths', image)

    def _get_name_emission_wavelengths(self, image):
        return self._get_metadata_from_name('_EM=', '_', list, image)

    # TODO: to move to interface
    def _get_objective_lens_refractive_index(self, image):
        objective_settings = image.getObjectiveSettings()
        if objective_settings is None:
            return None
        return objective_settings.getRefractiveIndex()

    def _get_objective_lens_na(self, image):
        objective_settings = image.getObjectiveSettings()
        if objective_settings is None:
            return None
        objective = objective_settings.getObjective()
        if objective is None:
            return None
        return objective.getLensNA()

    def _get_objective_lens_nominal_magnification(self, image):
        objective_settings = image.getObjectiveSettings()
        if objective_settings is None:
            return None
        objective = objective_settings.getObjective()
        if objective is None:
            return None
        return objective.getNominalMagnification()

    def _get_excitation_wavelengths(self, image):
        channels = image.getChannels()
        if channels is None:
            return None
        wavelengths = tuple(ch.getExcitationWave() for ch in channels)
        if not all(wavelengths):
            return None
        else:
            return wavelengths

    def _get_emission_wavelengths(self, image):
        channels = image.getChannels()
        if channels is None:
            return None
        wavelengths = tuple(ch.getEmissionWave() for ch in channels)
        if not all(wavelengths):
            return None
        else:
            return wavelengths

    def get_theoretical_res_fwhm(self, **kwargs):
        """Returns a dictionary containing the lateral and axial FWHM theoretical resolutions for every channel in image.

        :param image: an image object
        :return: a dictionary in the form:
                 {'resolution_theoretical_fwhm_lateral': list of lateral resolutions for every channel,
                  'resolution_theoretical_fwhm_axial': list of axial resolutions for every channel,
                  'resolution_theoretical_fwhm_units': string specifying units}
        """
        theoretical_res = {'resolution_theoretical_fwhm_lateral': [],
                           'resolution_theoretical_fwhm_axial': [],
                           'resolution_theoretical_fwhm_units': 'NANOMETER'}
        na = self.get_setting('objective_lens_na', **kwargs)
        for em in self.get_setting('emission_wavelengths', **kwargs):
            try:
                theoretical_res['resolution_theoretical_fwhm_lateral'].append(.353 * em / na)
            except TypeError as e:
                module_logger.warning('FWHM theoretical resolution could not be calculated. Verify configuration files.')
                theoretical_res['resolution_theoretical_fwhm_lateral'].append(None)
            try:
                theoretical_res['resolution_theoretical_fwhm_axial'].append(None)  # TODO: find formula for fwhm axial resolution
            except TypeError as e:
                module_logger.warning('FWHM theoretical resolution could not be calculated. Verify configuration files.')
                theoretical_res['resolution_theoretical_fwhm_axial'].append(None)

        return theoretical_res

    def get_theoretical_res_rayleigh(self, **kwargs):
        """Returns a dictionary containing the lateral and axial Rayleigh theoretical resolutions for every channel in image.

        :param image: an image object
        :return: a dictionary in the form:
                 {'resolution_theoretical_rayleigh_lateral': list of lateral resolutions for every channel,
                  'resolution_theoretical_rayleigh_axial': list of axial resolutions for every channel,
                  'resolution_theoretical_rayleigh_units': string specifying units}
        """
        theoretical_res = {'resolution_theoretical_rayleigh_lateral': [],
                           'resolution_theoretical_rayleigh_axial': [],
                           'resolution_theoretical_rayleigh_units': 'NANOMETER'}
        na = self.get_setting('objective_lens_na', **kwargs)
        ri = self.get_setting('objective_lens_refractive_index', **kwargs)
        for em in self.get_setting('emission_wavelengths', **kwargs):
            try:
                theoretical_res['resolution_theoretical_rayleigh_lateral'].append(.61 * em / na)
            except TypeError as e:
                module_logger.warning('Rayleigh theoretical lateral resolution could not be calculated. Verify configuration files.')
                theoretical_res['resolution_theoretical_rayleigh_lateral'].append(None)
            try:
                theoretical_res['resolution_theoretical_rayleigh_axial'].append(2 * em * ri / na ** 2)
            except TypeError as e:
                module_logger.warning('Rayleigh theoretical axial resolution could not be calculated. Verify configuration files.')
                theoretical_res['resolution_theoretical_rayleigh_axial'].append(None)

        return theoretical_res

    def get_theoretical_res(self, **kwargs):
        """Returns a dictionary containing the theoretical resolutions for every channel in image in all the
        implemented methods."""
        theoretical_res = dict()
        theoretical_res.update(self.get_theoretical_res_fwhm(**kwargs))
        theoretical_res.update(self.get_theoretical_res_rayleigh(**kwargs))

        return theoretical_res

    def get_nyquist(self, **kwargs):
        """Returns a dictionary containing the nyquist sampling criteria values for every channel in image.
        """
        nyquist_delta = {'nyquist_lateral': [],
                         'nyquist_axial': [],
                         'nyquist_units': 'NANOMETER'}  # TODO: FIX units for nyquist and resolution. Get them from wavelength units.
        na = self.get_setting('objective_lens_na', **kwargs)
        ri = self.get_setting('objective_lens_refractive_index', **kwargs)
        for em in self.get_setting('emission_wavelengths', **kwargs):
            try:
                nyquist_delta['nyquist_lateral'].append(em / (4 * ri * (na / ri)))
            except TypeError as e:
                module_logger.warning('Lateral Nyquist criterion could not be calculated. Verify configuration files.')
                nyquist_delta['nyquist_lateral'].append(None)
            try:
                nyquist_delta['nyquist_axial'].append(em / (2 * ri * (1 - cos(asin(na / ri)))))
            except TypeError as e:
                module_logger.warning('Axial Nyquist criterion could not be calculated. Verify configuration files.')
                nyquist_delta['nyquist_axial'].append(None)

        return nyquist_delta
        #
        # def calculate_theoretcal_resolution(microscope_type, na, refractive_index, emission_wave, excitation_wave=None):
        #     theoretical_res = {}
        #     if microscope_type is None:
        #         module_logger.warning(
        #             'Microscope type undefined to calculate theoretical resolution. Falling back into Wide-Field')
        #         theoretical_res = calculate_theoretcal_resolution('wf', na, refractive_index, emission_wave,
        #                                                           excitation_wave=excitation_wave)
        #     elif microscope_type.lower() in ['wf', 'wide-field', 'widefield']:
        #         theoretical_res['fwhm_lateral'] = .353 * emission_wave / na
        #         theoretical_res['rayleigh_lateral'] = .61 * emission_wave / na
        #         theoretical_res['rayleigh_axial'] = 2 * emission_wave * refractive_index / na ** 2
        #         theoretical_res['units'] = 'NANOMETER'
        #     elif microscope_type.lower() in ['confocal']:
        #         theoretical_res['fwhm_lateral'] = .353 * emission_wave / na
        #         theoretical_res['rayleigh_lateral'] = .4 * emission_wave / na
        #         theoretical_res['rayleigh_axial'] = 1.4 * emission_wave * refractive_index / na ** 2
        #         theoretical_res['units'] = 'NANOMETER'
        #     else:
        #         module_logger.warning(
        #             'Could not find microscope type to calculate theoretical resolution. Falling back into Wide-Field')
        #         theoretical_res = calculate_theoretcal_resolution('wf', na, refractive_index, emission_wave,
        #                                                           excitation_wave=excitation_wave)
        #
        #     return theoretical_res

        #
        # if refractive_index is None:
        #     module_logger.warning('Refractive index is being guessed. Nyquist criteria will not be correct.')
        #     if na > .8:
        #         refractive_index = 1.5
        #     else:
        #         refractive_index = 1.0
        # alpha = asin(na / refractive_index)
        # # Theoretical resolutions for confocal microscope are only attained with very closed pinhole
        # # We add a tolerance factor to render theoretical resolution more realistic
        # tolerance = 1.6
        # nyquist_delta = {}
        # if microscope_type is None:
        #     module_logger.warning(
        #         'Microscope type undefined to calculate Nyquist criterion. Falling back into Wide-Field')
        #     nyquist_delta = calculate_nyquist('wf', na, refractive_index, emission_wave, excitation_wave)
        # elif microscope_type.lower() in ['wf', 'wide-field', 'widefield']:
        #     nyquist_delta['lateral'] = emission_wave / (4 * refractive_index * sin(alpha))
        #     nyquist_delta['axial'] = emission_wave / (2 * refractive_index * (1 - cos(alpha)))
        #     nyquist_delta['units'] = 'NANOMETER'
        # elif microscope_type.lower() in ['confocal']:
        #     nyquist_delta['lateral'] = tolerance * excitation_wave / (8 * refractive_index * sin(alpha))
        #     nyquist_delta['axial'] = tolerance * excitation_wave / (4 * refractive_index * (1 - cos(alpha)))
        #     nyquist_delta['units'] = 'NANOMETER'
        # else:
        #     module_logger.warning(
        #         'Could not find microscope type to calculate Nyquist criterion. Falling back into Wide-Field')
        #     nyquist_delta = calculate_nyquist('wf', na, refractive_index, emission_wave, excitation_wave)
        #
        # return nyquist_delta

# from microscopemetrics.interface import omero as interface # Creating logging services Call callables, or return value of non-callables. The code of this class was copied from https://github.com/MicronOxford/microscope # Settings classes should be private: devices should use a factory method # rather than instantiate settings directly; most already use add_setting # for this. # TODO: Implement a get units Create a setting.         :param name: the setting's name         :param dtype: a data type from ('int', 'float', 'bool', 'enum', 'str')         :param get_from_db_func: a function to get the value from the database interface         :param get_from_conf_func: a function to get the value from the device configuration         :param get_from_name_func: a function to get the value from the image name         :param values: a description of allowed values dependent on dtype,                        or function that returns a description         Setters and getters accept or return:             the setting value for int, float, bool and str;             the setting index into a list, dict or Enum type for enum. Returns a dictionary describing the setting Get a setting # self._values is a list or tuple A superclass for all the microscope devices and eventually other type of devices. Add a setting definition.         :param name: the setting's name         :param dtype: a data type from ('int', 'float', 'bool', 'enum', 'str')         :param get_from_db_func: a function to get the value from the interface to the database         :param get_from_conf_func: a function to get the value from the configuration         :param get_from_name_func: a function to get the value from the image name         :param set_func: a function to set the value         :param values: a description of allowed values dependent on dtype,                        or function that returns a description         A client needs some way of knowing a setting name and data type,         retrieving the current value and, if settable, a way to retrieve         allowable values, and set the value. Tries to get a specified setting from the following sources by order of preference:         - Image from the database interface         - The microscope analysis_config linked through the image name         - Directly in the image name Return settings as a dict. Set a setting. Return ordered setting descriptions as a list of dicts. Return ordered setting descriptions as a list of dicts. Update settings based on dict of settings and values. # Assume nothing about state: set everything. # Only update changed values. # Update values. # Setting not recognised or no set function implemented # Read back values in second loop. A superclass for the microscopes. Inherit this class when you create a new type of microscope. # token not in name # Sort channels according to their position in the name A Widefield microscope # Setting some objective lens settings # get_from_db_func=interface._get_objective_lens_refractive_index, # get_from_db_func=interface._get_objective_lens_na, # get_from_db_func=interface._get_objective_lens_nominal_magnification, # Setting some channel settings # get_from_db_func=interface._get_excitation_wavelengths, # get_from_db_func=interface._get_emission_wavelengths, # TODO: to move to interface Returns a dictionary containing the lateral and axial FWHM theoretical resolutions for every channel in image.          :param image: an image object         :return: a dictionary in the form:                  {'resolution_theoretical_fwhm_lateral': list of lateral resolutions for every channel,                   'resolution_theoretical_fwhm_axial': list of axial resolutions for every channel,                   'resolution_theoretical_fwhm_units': string specifying units} # TODO: find formula for fwhm axial resolution Returns a dictionary containing the lateral and axial Rayleigh theoretical resolutions for every channel in image.          :param image: an image object         :return: a dictionary in the form:                  {'resolution_theoretical_rayleigh_lateral': list of lateral resolutions for every channel,                   'resolution_theoretical_rayleigh_axial': list of axial resolutions for every channel,                   'resolution_theoretical_rayleigh_units': string specifying units} Returns a dictionary containing the theoretical resolutions for every channel in image in all the         implemented methods. Returns a dictionary containing the nyquist sampling criteria values for every channel in image. # TODO: FIX units for nyquist and resolution. Get them from wavelength units. # # def calculate_theoretcal_resolution(microscope_type, na, refractive_index, emission_wave, excitation_wave=None): #     theoretical_res = {} #     if microscope_type is None: #         module_logger.warning( #             'Microscope type undefined to calculate theoretical resolution. Falling back into Wide-Field') #         theoretical_res = calculate_theoretcal_resolution('wf', na, refractive_index, emission_wave, #                                                           excitation_wave=excitation_wave) #     elif microscope_type.lower() in ['wf', 'wide-field', 'widefield']: #         theoretical_res['fwhm_lateral'] = .353 * emission_wave / na #         theoretical_res['rayleigh_lateral'] = .61 * emission_wave / na #         theoretical_res['rayleigh_axial'] = 2 * emission_wave * refractive_index / na ** 2 #         theoretical_res['units'] = 'NANOMETER' #     elif microscope_type.lower() in ['confocal']: #         theoretical_res['fwhm_lateral'] = .353 * emission_wave / na #         theoretical_res['rayleigh_lateral'] = .4 * emission_wave / na #         theoretical_res['rayleigh_axial'] = 1.4 * emission_wave * refractive_index / na ** 2 #         theoretical_res['units'] = 'NANOMETER' #     else: #         module_logger.warning( #             'Could not find microscope type to calculate theoretical resolution. Falling back into Wide-Field') #         theoretical_res = calculate_theoretcal_resolution('wf', na, refractive_index, emission_wave, #                                                           excitation_wave=excitation_wave) # #     return theoretical_res # # if refractive_index is None: #     module_logger.warning('Refractive index is being guessed. Nyquist criteria will not be correct.') #     if na > .8: #         refractive_index = 1.5 #     else: #         refractive_index = 1.0 # alpha = asin(na / refractive_index) # # Theoretical resolutions for confocal microscope are only attained with very closed pinhole # # We add a tolerance factor to render theoretical resolution more realistic # tolerance = 1.6 # nyquist_delta = {} # if microscope_type is None: #     module_logger.warning( #         'Microscope type undefined to calculate Nyquist criterion. Falling back into Wide-Field') #     nyquist_delta = calculate_nyquist('wf', na, refractive_index, emission_wave, excitation_wave) # elif microscope_type.lower() in ['wf', 'wide-field', 'widefield']: #     nyquist_delta['lateral'] = emission_wave / (4 * refractive_index * sin(alpha)) #     nyquist_delta['axial'] = emission_wave / (2 * refractive_index * (1 - cos(alpha))) #     nyquist_delta['units'] = 'NANOMETER' # elif microscope_type.lower() in ['confocal']: #     nyquist_delta['lateral'] = tolerance * excitation_wave / (8 * refractive_index * sin(alpha)) #     nyquist_delta['axial'] = tolerance * excitation_wave / (4 * refractive_index * (1 - cos(alpha))) #     nyquist_delta['units'] = 'NANOMETER' # else: #     module_logger.warning( #         'Could not find microscope type to calculate Nyquist criterion. Falling back into Wide-Field') #     nyquist_delta = calculate_nyquist('wf', na, refractive_index, emission_wave, excitation_wave) # # return nyquist_delta