Beispiel #1
0
def _update_phase_info(
    metadata: DictionaryTreeBrowser, dictionary: dict, phase_number: int = 1
) -> DictionaryTreeBrowser:
    """Update information of phase in metadata, adding it if it doesn't
    already exist.

    Parameters
    ----------
    metadata
        Metadata to update.
    dictionary
        Dictionary with only values to update.
    phase_number
        Number of phase to update.

    Returns
    -------
    metadata : DictionaryTreeBrowser
        Updated metadata.
    """
    # Check if metadata has phases
    if not metadata.has_item("Sample.Phases"):
        metadata.add_node("Sample.Phases")

    # Check if phase number is already in metadata
    phase = metadata.Sample.Phases.get_item(str(phase_number))
    if phase is None:
        phase = _phase_metadata()
    phase = dict(phase)

    # Loop over input dictionary and update items in phase dictionary
    for key, val in dictionary.items():
        key = re.sub(r"(\w)([A-Z])", r"\1 \2", key)  # Space before UPPERCASE
        key = key.lower()
        key = key.replace(" ", "_")
        if key in phase:
            if isinstance(val, list):
                val = np.array(val)
            phase[key] = val

    # Update phase info in metadata
    metadata.Sample.Phases.add_dictionary({str(phase_number): phase})

    return metadata
Beispiel #2
0
def parse_msa_string(string, filename=None):
    """Parse an EMSA/MSA file content.

    Parameters
    ----------
    string: string or file object
        It must complain with the EMSA/MSA standard.
    filename: string or None
        The filename.

    Returns:
    --------
    file_data_list: list
        The list containts a dictionary that contains the parsed
        information. It can be used to create a `:class:Signal`
        using `:func:hyperspy.io.dict2signal`.

    """
    if not hasattr(string, "readlines"):
        string = string.splitlines()
    parameters = {}
    mapped = DictionaryTreeBrowser({})
    y = []
    # Read the keywords
    data_section = False
    for line in string:
        if data_section is False:
            if line[0] == "#":
                try:
                    key, value = line.split(': ')
                    value = value.strip()
                except ValueError:
                    key = line
                    value = None
                key = key.strip('#').strip()

                if key != 'SPECTRUM':
                    parameters[key] = value
                else:
                    data_section = True
        else:
            # Read the data
            if line[0] != "#" and line.strip():
                if parameters['DATATYPE'] == 'XY':
                    xy = line.replace(',', ' ').strip().split()
                    y.append(float(xy[1]))
                elif parameters['DATATYPE'] == 'Y':
                    data = [
                        float(i) for i in line.replace(',', ' ').strip().split()]
                    y.extend(data)
    # We rewrite the format value to be sure that it complies with the
    # standard, because it will be used by the writer routine
    parameters['FORMAT'] = "EMSA/MAS Spectral Data File"

    # Convert the parameters to the right type and map some
    # TODO: the msa format seems to support specifying the units of some
    # parametes. We should add this feature here
    for parameter, value in parameters.items():
        # Some parameters names can contain the units information
        # e.g. #AZIMANGLE-dg: 90.
        if '-' in parameter:
            clean_par, units = parameter.split('-')
            clean_par, units = clean_par.strip(), units.strip()
        else:
            clean_par, units = parameter, None
        if clean_par in keywords:
            try:
                parameters[parameter] = keywords[clean_par]['dtype'](value)
            except:
                # Normally the offending mispelling is a space in the scientic
                # notation, e.g. 2.0 E-06, so we try to correct for it
                try:
                    parameters[parameter] = keywords[clean_par]['dtype'](
                        value.replace(' ', ''))
                except:
                    _logger.exception(
                        "The %s keyword value, %s could not be converted to "
                        "the right type", parameter, value)

            if keywords[clean_par]['mapped_to'] is not None:
                mapped.set_item(keywords[clean_par]['mapped_to'],
                                parameters[parameter])
                if units is not None:
                    mapped.set_item(keywords[clean_par]['mapped_to'] +
                                    '_units', units)

    # The data parameter needs some extra care
    # It is necessary to change the locale to US english to read the date
    # keyword
    loc = locale.getlocale(locale.LC_TIME)
    # Setting locale can raise an exception because
    # their name depends on library versions, platform etc.
    try:
        if os_name == 'posix':
            locale.setlocale(locale.LC_TIME, ('en_US', 'utf8'))
        elif os_name == 'windows':
            locale.setlocale(locale.LC_TIME, 'english')
        try:
            H, M = time.strptime(parameters['TIME'], "%H:%M")[3:5]
            mapped.set_item('General.time', datetime.time(H, M))
        except:
            if 'TIME' in parameters and parameters['TIME']:
                _logger.warn('The time information could not be retrieved')
        try:
            Y, M, D = time.strptime(parameters['DATE'], "%d-%b-%Y")[0:3]
            mapped.set_item('General.date', datetime.date(Y, M, D))
        except:
            if 'DATE' in parameters and parameters['DATE']:
                _logger.warn('The date information could not be retrieved')
    except:
        warnings.warn("I couldn't write the date information due to"
                      "an unexpected error. Please report this error to "
                      "the developers")
    locale.setlocale(locale.LC_TIME, loc)  # restore saved locale

    axes = [{
        'size': len(y),
        'index_in_array': 0,
        'name': parameters['XLABEL'] if 'XLABEL' in parameters else '',
        'scale': parameters['XPERCHAN'] if 'XPERCHAN' in parameters else 1,
        'offset': parameters['OFFSET'] if 'OFFSET' in parameters else 0,
        'units': parameters['XUNITS'] if 'XUNITS' in parameters else '',
    }]
    if filename is not None:
        mapped.set_item('General.original_filename',
                        os.path.split(filename)[1])
    mapped.set_item('Signal.record_by', 'spectrum')
    if mapped.has_item('Signal.signal_type'):
        if mapped.Signal.signal_type == 'ELS':
            mapped.Signal.signal_type = 'EELS'
    else:
        # Defaulting to EELS looks reasonable
        mapped.set_item('Signal.signal_type', 'EELS')

    dictionary = {
        'data': np.array(y),
        'axes': axes,
        'metadata': mapped.as_dictionary(),
        'original_metadata': parameters
    }
    file_data_list = [dictionary, ]
    return file_data_list
Beispiel #3
0
def file_reader(filename, encoding='latin-1', **kwds):
    parameters = {}
    mapped = DictionaryTreeBrowser({})
    with codecs.open(
            filename,
            encoding=encoding,
            errors='replace') as spectrum_file:
        y = []
        # Read the keywords
        data_section = False
        for line in spectrum_file.readlines():
            if data_section is False:
                if line[0] == "#":
                    try:
                        key, value = line.split(': ')
                        value = value.strip()
                    except ValueError:
                        key = line
                        value = None
                    key = key.strip('#').strip()

                    if key != 'SPECTRUM':
                        parameters[key] = value
                    else:
                        data_section = True
            else:
                # Read the data
                if line[0] != "#" and line.strip():
                    if parameters['DATATYPE'] == 'XY':
                        xy = line.replace(',', ' ').strip().split()
                        y.append(float(xy[1]))
                    elif parameters['DATATYPE'] == 'Y':
                        data = [
                            float(i) for i in line.replace(
                                ',',
                                ' ').strip().split()]
                        y.extend(data)
    # We rewrite the format value to be sure that it complies with the
    # standard, because it will be used by the writer routine
    parameters['FORMAT'] = "EMSA/MAS Spectral Data File"

    # Convert the parameters to the right type and map some
    # TODO: the msa format seems to support specifying the units of some
    # parametes. We should add this feature here
    for parameter, value in parameters.iteritems():
        # Some parameters names can contain the units information
        # e.g. #AZIMANGLE-dg: 90.
        if '-' in parameter:
            clean_par, units = parameter.split('-')
            clean_par, units = clean_par.strip(), units.strip()
        else:
            clean_par, units = parameter, None
        if clean_par in keywords:
            try:
                parameters[parameter] = keywords[clean_par]['dtype'](value)
            except:
                # Normally the offending mispelling is a space in the scientic
                # notation, e.g. 2.0 E-06, so we try to correct for it
                try:
                    parameters[parameter] = keywords[clean_par]['dtype'](
                        value.replace(' ', ''))
                except:
                    print("The %s keyword value, %s " % (parameter, value) +
                          "could not be converted to the right type")

            if keywords[clean_par]['mapped_to'] is not None:
                mapped.set_item(keywords[clean_par]['mapped_to'],
                                parameters[parameter])
                if units is not None:
                    mapped.set_item(keywords[clean_par]['mapped_to'] +
                                    '_units', units)

    # The data parameter needs some extra care
    # It is necessary to change the locale to US english to read the date
    # keyword
    loc = locale.getlocale(locale.LC_TIME)
    # Setting locale can raise an exception because
    # their name depends on library versions, platform etc.
    try:
        if os_name == 'posix':
            locale.setlocale(locale.LC_TIME, ('en_US', 'utf8'))
        elif os_name == 'windows':
            locale.setlocale(locale.LC_TIME, 'english')
        try:
            H, M = time.strptime(parameters['TIME'], "%H:%M")[3:5]
            mapped.set_item('General.time', datetime.time(H, M))
        except:
            if 'TIME' in parameters and parameters['TIME']:
                print('The time information could not be retrieved')
        try:
            Y, M, D = time.strptime(parameters['DATE'], "%d-%b-%Y")[0:3]
            mapped.set_item('General.date', datetime.date(Y, M, D))
        except:
            if 'DATE' in parameters and parameters['DATE']:
                print('The date information could not be retrieved')
    except:
        warnings.warn("I couldn't write the date information due to"
                      "an unexpected error. Please report this error to "
                      "the developers")
    locale.setlocale(locale.LC_TIME, loc)  # restore saved locale

    axes = [{
        'size': len(y),
        'index_in_array': 0,
        'name': parameters['XLABEL'] if 'XLABEL' in parameters else '',
        'scale': parameters['XPERCHAN'] if 'XPERCHAN' in parameters else 1,
        'offset': parameters['OFFSET'] if 'OFFSET' in parameters else 0,
        'units': parameters['XUNITS'] if 'XUNITS' in parameters else '',
    }]

    mapped.set_item('General.original_filename', os.path.split(filename)[1])
    mapped.set_item('Signal.record_by', 'spectrum')
    if mapped.has_item('Signal.signal_type'):
        if mapped.Signal.signal_type == 'ELS':
            mapped.Signal.signal_type = 'EELS'
    else:
        # Defaulting to EELS looks reasonable
        mapped.set_item('Signal.signal_type', 'EELS')

    dictionary = {
        'data': np.array(y),
        'axes': axes,
        'metadata': mapped.as_dictionary(),
        'original_metadata': parameters
    }
    return [dictionary, ]
Beispiel #4
0
def parse_msa_string(string, filename=None):
    """Parse an EMSA/MSA file content.

    Parameters
    ----------
    string: string or file object
        It must complain with the EMSA/MSA standard.
    filename: string or None
        The filename.

    Returns:
    --------
    file_data_list: list
        The list containts a dictionary that contains the parsed
        information. It can be used to create a `:class:BaseSignal`
        using `:func:hyperspy.io.dict2signal`.

    """
    if not hasattr(string, "readlines"):
        string = string.splitlines()
    parameters = {}
    mapped = DictionaryTreeBrowser({})
    y = []
    # Read the keywords
    data_section = False
    for line in string:
        if data_section is False:
            if line[0] == "#":
                try:
                    key, value = line.split(': ')
                    value = value.strip()
                except ValueError:
                    key = line
                    value = None
                key = key.strip('#').strip()

                if key != 'SPECTRUM':
                    parameters[key] = value
                else:
                    data_section = True
        else:
            # Read the data
            if line[0] != "#" and line.strip():
                if parameters['DATATYPE'] == 'XY':
                    xy = line.replace(',', ' ').strip().split()
                    y.append(float(xy[1]))
                elif parameters['DATATYPE'] == 'Y':
                    data = [
                        float(i) for i in line.replace(',', ' ').strip().split()]
                    y.extend(data)
    # We rewrite the format value to be sure that it complies with the
    # standard, because it will be used by the writer routine
    parameters['FORMAT'] = "EMSA/MAS Spectral Data File"

    # Convert the parameters to the right type and map some
    # TODO: the msa format seems to support specifying the units of some
    # parametes. We should add this feature here
    for parameter, value in parameters.items():
        # Some parameters names can contain the units information
        # e.g. #AZIMANGLE-dg: 90.
        if '-' in parameter:
            clean_par, units = parameter.split('-')
            clean_par, units = clean_par.strip(), units.strip()
        else:
            clean_par, units = parameter, None
        if clean_par in keywords:
            try:
                parameters[parameter] = keywords[clean_par]['dtype'](value)
            except BaseException:
                # Normally the offending mispelling is a space in the scientic
                # notation, e.g. 2.0 E-06, so we try to correct for it
                try:
                    parameters[parameter] = keywords[clean_par]['dtype'](
                        value.replace(' ', ''))
                except BaseException:
                    _logger.exception(
                        "The %s keyword value, %s could not be converted to "
                        "the right type", parameter, value)

            if keywords[clean_par]['mapped_to'] is not None:
                mapped.set_item(keywords[clean_par]['mapped_to'],
                                parameters[parameter])
                if units is not None:
                    mapped.set_item(keywords[clean_par]['mapped_to'] +
                                    '_units', units)

    # The data parameter needs some extra care
    # It is necessary to change the locale to US english to read the date
    # keyword
    # Setting locale can raise an exception because
    # their name depends on library versions, platform etc.
    # https://docs.python.org/3.7/library/locale.html
    try:
        if os_name == 'posix':
            locale.setlocale(locale.LC_TIME, ('en_US', 'utf8'))
        elif os_name == 'windows':
            locale.setlocale(locale.LC_TIME, 'english')
        try:
            time = dt.strptime(parameters['TIME'], "%H:%M")
            mapped.set_item('General.time', time.time().isoformat())
        except BaseException:
            if 'TIME' in parameters and parameters['TIME']:
                _logger.warning('The time information could not be retrieved.')
        try:
            date = dt.strptime(parameters['DATE'], "%d-%b-%Y")
            mapped.set_item('General.date', date.date().isoformat())
        except BaseException:
            if 'DATE' in parameters and parameters['DATE']:
                _logger.warning('The date information could not be retrieved.')
    except locale.Error:
        _logger.warning("The date and time could not be retrieved because the "
                        "english US locale is not installed on the system.")
    locale.setlocale(locale.LC_TIME, '')  # restore user’s default settings 

    axes = [{
        'size': len(y),
        'index_in_array': 0,
        'name': parameters['XLABEL'] if 'XLABEL' in parameters else '',
        'scale': parameters['XPERCHAN'] if 'XPERCHAN' in parameters else 1,
        'offset': parameters['OFFSET'] if 'OFFSET' in parameters else 0,
        'units': parameters['XUNITS'] if 'XUNITS' in parameters else '',
    }]
    if filename is not None:
        mapped.set_item('General.original_filename',
                        os.path.split(filename)[1])
    mapped.set_item('Signal.record_by', 'spectrum')
    if mapped.has_item('Signal.signal_type'):
        if mapped.Signal.signal_type == 'ELS':
            mapped.Signal.signal_type = 'EELS'
        if mapped.Signal.signal_type in ['EDX', 'XEDS']:
            mapped.Signal.signal_type = 'EDS'
    else:
        # Defaulting to EELS looks reasonable
        mapped.set_item('Signal.signal_type', 'EELS')
    if 'YUNITS' in parameters.keys():
        yunits = "(%s)" % parameters['YUNITS']
    else:
        yunits = ""
    if 'YLABEL' in parameters.keys():
        quantity = "%s" % parameters['YLABEL']
    else:
        if mapped.Signal.signal_type == 'EELS':
            quantity = 'Electrons'
            if not yunits:
                yunits = "(Counts)"
        elif 'EDS' in mapped.Signal.signal_type:
            quantity = 'X-rays'
            if not yunits:
                yunits = "(Counts)"
        else:
            quantity = ""
    if quantity or yunits:
        quantity_units = "%s %s" % (quantity, yunits)
        mapped.set_item('Signal.quantity', quantity_units.strip())

    dictionary = {
        'data': np.array(y),
        'axes': axes,
        'metadata': mapped.as_dictionary(),
        'original_metadata': parameters
    }
    file_data_list = [dictionary, ]
    return file_data_list
Beispiel #5
0
def parse_msa_string(string, filename=None):
    """Parse an EMSA/MSA file content.

    Parameters
    ----------
    string: string or file object
        It must complain with the EMSA/MSA standard.
    filename: string or None
        The filename.

    Returns:
    --------
    file_data_list: list
        The list containts a dictionary that contains the parsed
        information. It can be used to create a `:class:BaseSignal`
        using `:func:hyperspy.io.dict2signal`.

    """
    if not hasattr(string, "readlines"):
        string = string.splitlines()
    parameters = {}
    mapped = DictionaryTreeBrowser({})
    y = []
    # Read the keywords
    data_section = False
    for line in string:
        if data_section is False:
            if line[0] == "#":
                try:
                    key, value = line.split(': ')
                    value = value.strip()
                except ValueError:
                    key = line
                    value = None
                key = key.strip('#').strip()

                if key != 'SPECTRUM':
                    parameters[key] = value
                else:
                    data_section = True
        else:
            # Read the data
            if line[0] != "#" and line.strip():
                if parameters['DATATYPE'] == 'XY':
                    xy = line.replace(',', ' ').strip().split()
                    y.append(float(xy[1]))
                elif parameters['DATATYPE'] == 'Y':
                    data = [
                        float(i)
                        for i in line.replace(',', ' ').strip().split()
                    ]
                    y.extend(data)
    # We rewrite the format value to be sure that it complies with the
    # standard, because it will be used by the writer routine
    parameters['FORMAT'] = "EMSA/MAS Spectral Data File"

    # Convert the parameters to the right type and map some
    # TODO: the msa format seems to support specifying the units of some
    # parametes. We should add this feature here
    for parameter, value in parameters.items():
        # Some parameters names can contain the units information
        # e.g. #AZIMANGLE-dg: 90.
        if '-' in parameter:
            clean_par, units = parameter.split('-')
            clean_par, units = clean_par.strip(), units.strip()
        else:
            clean_par, units = parameter, None
        if clean_par in keywords:
            try:
                parameters[parameter] = keywords[clean_par]['dtype'](value)
            except BaseException:
                # Normally the offending mispelling is a space in the scientic
                # notation, e.g. 2.0 E-06, so we try to correct for it
                try:
                    parameters[parameter] = keywords[clean_par]['dtype'](
                        value.replace(' ', ''))
                except BaseException:
                    _logger.exception(
                        "The %s keyword value, %s could not be converted to "
                        "the right type", parameter, value)

            if keywords[clean_par]['mapped_to'] is not None:
                mapped.set_item(keywords[clean_par]['mapped_to'],
                                parameters[parameter])
                if units is not None:
                    mapped.set_item(
                        keywords[clean_par]['mapped_to'] + '_units', units)
    if 'TIME' in parameters and parameters['TIME']:
        try:
            time = dt.strptime(parameters['TIME'], "%H:%M")
            mapped.set_item('General.time', time.time().isoformat())
        except ValueError as e:
            _logger.warning(
                'Possible malformed TIME field in msa file. The time information could not be retrieved.: %s'
                % e)
    else:
        _logger.warning('TIME information missing.')

    malformed_date_error = 'Possibly malformed DATE in msa file. The date information could not be retrieved.'
    if "DATE" in parameters and parameters["DATE"]:
        try:
            day, month, year = parameters["DATE"].split("-")
            if month.upper() in US_MONTH_A2D:
                month = US_MONTH_A2D[month.upper()]
                date = dt.strptime("-".join((day, month, year)), "%d-%m-%Y")
                mapped.set_item('General.date', date.date().isoformat())
            else:
                _logger.warning(malformed_date_error)
        except ValueError as e:  # Error raised if split does not return 3 elements in this case
            _logger.warning(malformed_date_error + ": %s" % e)

    axes = [{
        'size': len(y),
        'index_in_array': 0,
        'name': parameters['XLABEL'] if 'XLABEL' in parameters else '',
        'scale': parameters['XPERCHAN'] if 'XPERCHAN' in parameters else 1,
        'offset': parameters['OFFSET'] if 'OFFSET' in parameters else 0,
        'units': parameters['XUNITS'] if 'XUNITS' in parameters else '',
    }]
    if filename is not None:
        mapped.set_item('General.original_filename',
                        os.path.split(filename)[1])
    mapped.set_item('Signal.record_by', 'spectrum')
    if mapped.has_item('Signal.signal_type'):
        if mapped.Signal.signal_type == 'ELS':
            mapped.Signal.signal_type = 'EELS'
        if mapped.Signal.signal_type in ['EDX', 'XEDS']:
            mapped.Signal.signal_type = 'EDS'
    else:
        # Defaulting to EELS looks reasonable
        mapped.set_item('Signal.signal_type', 'EELS')
    if 'YUNITS' in parameters.keys():
        yunits = "(%s)" % parameters['YUNITS']
    else:
        yunits = ""
    if 'YLABEL' in parameters.keys():
        quantity = "%s" % parameters['YLABEL']
    else:
        if mapped.Signal.signal_type == 'EELS':
            quantity = 'Electrons'
            if not yunits:
                yunits = "(Counts)"
        elif 'EDS' in mapped.Signal.signal_type:
            quantity = 'X-rays'
            if not yunits:
                yunits = "(Counts)"
        else:
            quantity = ""
    if quantity or yunits:
        quantity_units = "%s %s" % (quantity, yunits)
        mapped.set_item('Signal.quantity', quantity_units.strip())

    dictionary = {
        'data': np.array(y),
        'axes': axes,
        'metadata': mapped.as_dictionary(),
        'original_metadata': parameters
    }
    file_data_list = [
        dictionary,
    ]
    return file_data_list
class EBSDMasterPattern(CommonImage, Signal2D):
    """Simulated Electron Backscatter Diffraction (EBSD) master pattern.

    This class extends HyperSpy's Signal2D class for EBSD master
    patterns.

    Methods inherited from HyperSpy can be found in the HyperSpy user
    guide.

    See the docstring of :class:`hyperspy.signal.BaseSignal` for a list
    of attributes.

    """

    _signal_type = "EBSDMasterPattern"
    _alias_signal_types = ["ebsd_master_pattern", "master_pattern"]
    _lazy = False

    def __init__(self, *args, **kwargs):
        """Create an :class:`~kikuchipy.signals.EBSDMasterPattern`
        object from a :class:`hyperspy.signals.Signal2D` or a
        :class:`numpy.ndarray`.

        """

        Signal2D.__init__(self, *args, **kwargs)

        # Update metadata if object is initialized from numpy array or
        # with set_signal_type()
        if not self.metadata.has_item(metadata_nodes("ebsd_master_pattern")):
            md = self.metadata.as_dictionary()
            md.update(ebsd_master_pattern_metadata().as_dictionary())
            self.metadata = DictionaryTreeBrowser(md)
        if not self.metadata.has_item("Sample.Phases"):
            self.set_phase_parameters()

    def set_simulation_parameters(
        self,
        complete_cutoff: Union[None, int, float] = None,
        depth_step: Union[None, int, float] = None,
        energy_step: Union[None, int, float] = None,
        hemisphere: Union[None, str] = None,
        incident_beam_energy: Union[None, int, float] = None,
        max_depth: Union[None, int, float] = None,
        min_beam_energy: Union[None, int, float] = None,
        mode: Optional[str] = None,
        number_of_electrons: Optional[int] = None,
        pixels_along_x: Optional[int] = None,
        projection: Union[None, str] = None,
        sample_tilt: Union[None, int, float] = None,
        smallest_interplanar_spacing: Union[None, int, float] = None,
        strong_beam_cutoff: Union[None, int, float] = None,
        weak_beam_cutoff: Union[None, int, float] = None,
    ):
        """Set simulated parameters in signal metadata.

        Parameters
        ----------
        complete_cutoff
            Bethe parameter c3.
        depth_step
            Material penetration depth step size, in nm.
        energy_step
            Energy bin size, in keV.
        hemisphere
            Which hemisphere(s) the data contains.
        incident_beam_energy
            Incident beam energy, in keV.
        max_depth
            Maximum material penetration depth, in nm.
        min_beam_energy
            Minimum electron energy to consider, in keV.
        mode
            Simulation mode, e.g. Continuous slowing down
            approximation (CSDA) used by EMsoft.
        number_of_electrons
            Total number of incident electrons.
        pixels_along_x
            Pixels along horizontal direction.
        projection
            Which projection the pattern is in.
        sample_tilt
            Sample tilte angle from horizontal, in degrees.
        smallest_interplanar_spacing
            Smallest interplanar spacing, d-spacing, taken into account
            in the computation of the electrostatic lattice potential,
            in nm.
        strong_beam_cutoff
            Bethe parameter c1.
        weak_beam_cutoff
            Bethe parameter c2.

        See Also
        --------
        set_phase_parameters

        Examples
        --------
        >>> import kikuchipy as kp
        >>> ebsd_mp_node = kp.signals.util.metadata_nodes(
        ...     "ebsd_master_pattern")
        >>> s.metadata.get_item(ebsd_mp_node + '.incident_beam_energy')
        15.0
        >>> s.set_simulated_parameters(incident_beam_energy=20.5)
        >>> s.metadata.get_item(ebsd_mp_node + '.incident_beam_energy')
        20.5
        """
        md = self.metadata
        ebsd_mp_node = metadata_nodes("ebsd_master_pattern")
        _write_parameters_to_dictionary(
            {
                "BSE_simulation": {
                    "depth_step": depth_step,
                    "energy_step": energy_step,
                    "incident_beam_energy": incident_beam_energy,
                    "max_depth": max_depth,
                    "min_beam_energy": min_beam_energy,
                    "mode": mode,
                    "number_of_electrons": number_of_electrons,
                    "pixels_along_x": pixels_along_x,
                    "sample_tilt": sample_tilt,
                },
                "Master_pattern": {
                    "Bethe_parameters": {
                        "complete_cutoff": complete_cutoff,
                        "strong_beam_cutoff": strong_beam_cutoff,
                        "weak_beam_cutoff": weak_beam_cutoff,
                    },
                    "smallest_interplanar_spacing":
                    smallest_interplanar_spacing,
                    "projection": projection,
                    "hemisphere": hemisphere,
                },
            },
            md,
            ebsd_mp_node,
        )

    def set_phase_parameters(
        self,
        number: int = 1,
        atom_coordinates: Optional[dict] = None,
        formula: Optional[str] = None,
        info: Optional[str] = None,
        lattice_constants: Union[None, np.ndarray, List[float],
                                 List[int]] = None,
        laue_group: Optional[str] = None,
        material_name: Optional[str] = None,
        point_group: Optional[str] = None,
        setting: Optional[int] = None,
        source: Optional[str] = None,
        space_group: Optional[int] = None,
        symmetry: Optional[int] = None,
    ):
        """Set parameters for one phase in signal metadata.

        A phase node with default values is created if none is present
        in the metadata when this method is called.

        Parameters
        ----------
        number
            Phase number.
        atom_coordinates
            Dictionary of dictionaries with one or more of the atoms in
            the unit cell, on the form `{'1': {'atom': 'Ni',
            'coordinates': [0, 0, 0], 'site_occupation': 1,
            'debye_waller_factor': 0}, '2': {'atom': 'O',... etc.`
            `debye_waller_factor` in units of nm^2, and
            `site_occupation` in range [0, 1].
        formula
            Phase formula, e.g. 'Fe2' or 'Ni'.
        info
            Whatever phase info the user finds relevant.
        lattice_constants
            Six lattice constants a, b, c, alpha, beta, gamma.
        laue_group
            Phase Laue group.
        material_name
            Name of material.
        point_group
            Phase point group.
        setting
            Space group's origin setting.
        source
            Literature reference for phase data.
        space_group
            Number between 1 and 230.
        symmetry
            Phase symmetry.

        See Also
        --------
        set_simulation_parameters

        Examples
        --------
        >>> s.metadata.Sample.Phases.Number_1.atom_coordinates.Number_1
        ├── atom =
        ├── coordinates = array([0., 0., 0.])
        ├── debye_waller_factor = 0.0
        └── site_occupation = 0.0
        >>> s.set_phase_parameters(
        ...     number=1, atom_coordinates={
        ...         '1': {'atom': 'Ni', 'coordinates': [0, 0, 0],
        ...         'site_occupation': 1,
        ...         'debye_waller_factor': 0.0035}})
        >>> s.metadata.Sample.Phases.Number_1.atom_coordinates.Number_1
        ├── atom = Ni
        ├── coordinates = array([0., 0., 0.])
        ├── debye_waller_factor = 0.0035
        └── site_occupation = 1
        """
        # Ensure atom coordinates are numpy arrays
        if atom_coordinates is not None:
            for phase, val in atom_coordinates.items():
                atom_coordinates[phase]["coordinates"] = np.array(
                    atom_coordinates[phase]["coordinates"])

        inputs = {
            "atom_coordinates": atom_coordinates,
            "formula": formula,
            "info": info,
            "lattice_constants": lattice_constants,
            "laue_group": laue_group,
            "material_name": material_name,
            "point_group": point_group,
            "setting": setting,
            "source": source,
            "space_group": space_group,
            "symmetry": symmetry,
        }

        # Remove None values
        phase = {k: v for k, v in inputs.items() if v is not None}
        _update_phase_info(self.metadata, phase, number)