Source code for CHAP.utils.material

#!/usr/bin/env python
#-*- coding: utf-8 -*-
"""Module defining the Material class."""

# System modules
from logging import getLogger
from os import path

# Third party modules
import numpy as np
from hexrd import material
from hexrd.valunits import valWUnit
HAVE_XU = False
HAVE_HEXRD = True
#try:
#    from xrayutilities import materials
#    from xrayutilities import simpack
#    HAVE_XU = True
#except ImportError:
#    HAVE_XU = False
#try:
#    from hexrd import material
#    HAVE_HEXRD = True
#except ImportError:
#    HAVE_HEXRD = False
#if HAVE_HEXRD:
#    try:
#        from hexrd.valunits import valWUnit
#    except ImportError:
#        raise
#        HAVE_HEXRD = False

POWDER_INTENSITY_CUTOFF = 1.e-8

logger = getLogger(__name__)


[docs] class Material: """Base class for materials in an sin2psi or EDD analysis. Right now it assumes a single material, extend its ability to do differently when test data is available. """ def __init__( self, material_name=None, material_file=None, sgnum=None, lattice_parameters_angstroms=None, atoms=None, pos=None, enrgy=None): """Initialize Material. :param material_name: Material name. :type material_name: str, optional :param material_file: Material file name in cif or hdf5 format. :type material_file: str, optional :param sgnum: Space group number. :type sgnum: int, optional :param lattice_parameters_angstroms: Lattice parameters in angstrom. :type lattice_parameters_angstroms: float, optional :param atoms: Elements. :type atoms: list, optional :param pos: Wyckoff atom positions in the unit cell. :type pos: list, optional :param enrgy: X-ray energy in eV. :type enrgy: float, optional """ self._enrgy = enrgy self._materials = [] self._ds_min = [] self._ds_unique = None self._hkls_unique = None if material_name is not None: self.add_material( material_name, material_file, sgnum, lattice_parameters_angstroms, atoms, pos)
[docs] def lattice_parameters(self, index=0): """Convert from internal nm units to angstrom. :param index: List index of the material, defaults to `0`. :type index: int, optional :return: Lattice parameters in angstrom. :rtype: list """ matl = self._materials[index] if isinstance(matl, materials.material.Crystal): return [matl.a, matl.b, matl.c] if isinstance(matl, material.Material): return [ lpars.getVal('angstrom') for lpars in self._materials[index].latticeParameters[0:3]] raise ValueError('Illegal material class type')
[docs] def ds_unique(self, tth_tol=None, tth_max=None, round_sig=8): """Return the unique lattice spacings. :param tth_tol: Two-theta tolerance (in degrees). :type tth_tol: float, optional :param tth_max: Maximum two-theta value (in degrees). :type tth_max: float, optional :param round_sig: Significant digits, passed to round() function, defaults to `8`. :type round_sig: int, optional :returns: Unique lattice spacings. :rtype: list """ if self._ds_unique is None: self.get_ds_unique(tth_tol, tth_max, round_sig) return self._ds_unique
[docs] def hkls_unique(self, tth_tol=None, tth_max=None, round_sig=8): """Return the unique HKLs. :param tth_tol: Two-theta tolerance (in degrees). :type tth_tol: float, optional :param tth_max: Maximum two-theta value (in degrees). :type tth_max: float, optional :param round_sig: Significant digits, passed to round() function, defaults to `8`. :type round_sig: int, optional :returns: HKLs corresponding to the unique lattice spacings. :rtype: list """ if self._hkls_unique is None: self.get_ds_unique(tth_tol, tth_max, round_sig) return self._hkls_unique
[docs] def add_material( self, material_name, material_file=None, sgnum=None, lattice_parameters_angstroms=None, atoms=None, pos=None, dmin_angstroms=0.35): """Add a material to the internal list of materials. :param material_name: Material name. :type material_name: str :param material_file: Material file name in cif or hdf5 format. :type material_file: str, optional :param sgnum: Space group number. :type sgnum: int, optional :param lattice_parameters_angstroms: Lattice parameters in angstrom. :type lattice_parameters_angstroms: float, optional :param atoms: Elements. :type atoms: list, optional :param pos: Wyckoff atom positions in the unit cell. :type pos: list, optional :param dmin_angstroms: Minimum lattice spacing in angstrom, defaults to `0.35`. :type dmin_angstroms: float, optional """ # At this point only for a single material # Unique energies works for more, but fitting with different # materials is not implemented if len(self._materials) == 1: raise ValueError('Multiple materials not implemented yet') self._ds_min.append(dmin_angstroms) self._materials.append( Material.make_material( material_name, material_file, sgnum, lattice_parameters_angstroms, atoms, pos, dmin_angstroms))
[docs] def get_ds_unique(self, tth_tol=None, tth_max=None, round_sig=8): """Get the list of unique lattice spacings from material HKLs. :param tth_tol: Two-theta tolerance (in degrees). :type tth_tol: float, optional :param tth_max: Maximum two-theta value (in degrees). :type tth_max: float, optional :param round_sig: Significant digits, passed to round() function, defaults to `8`. :type round_sig: int, optional :returns: HKLs corresponding to the unique lattice spacings and the list of the unique lattice spacings. :rtype: list, list """ hkls = np.empty((0,3)) ds = np.empty((0)) ds_index = np.empty((0)) for i, m in enumerate(self._materials): material_class_valid = False if HAVE_XU: if isinstance(m, materials.material.Crystal): if self._enrgy is None: powder = simpack.PowderDiffraction(m) else: powder = simpack.PowderDiffraction(m, en=self._enrgy) hklsi = [hkl for hkl in powder.data if powder.data[hkl]['active']] ds_i = [m.planeDistance(hkl) for hkl in powder.data if powder.data[hkl]['active']] mask = [d > self._ds_min[i] for d in ds_i] hkls = np.vstack((hkls, np.array(hklsi)[mask,:])) ds_i = np.array(ds_i)[mask] material_class_valid = True if HAVE_HEXRD: if isinstance(m, material.Material): plane_data = m.planeData if tth_tol is not None: plane_data.tThWidth = np.radians(tth_tol) if tth_max is not None: plane_data.exclusions = None plane_data.tThMax = np.radians(tth_max) hkls = np.vstack((hkls, plane_data.hkls.T)) ds_i = plane_data.getPlaneSpacings() material_class_valid = True if not material_class_valid: raise ValueError('Illegal material class type') ds = np.hstack((ds, ds_i)) ds_index = np.hstack((ds_index, i*np.ones(len(ds_i)))) # Sort lattice spacings in reverse order (use -) ds_unique, ds_index_unique, _ = np.unique( -ds.round(round_sig), return_index=True, return_counts=True) ds_unique = np.abs(ds_unique) # Limit the list to unique lattice spacings self._hkls_unique = hkls[ds_index_unique,:].astype(int) self._ds_unique = ds[ds_index_unique] hkl_list = np.vstack( (np.arange(self._ds_unique.shape[0]), ds_index[ds_index_unique], self._hkls_unique.T, self._ds_unique)).T logger.info("Unique d's:") for hkl in hkl_list: logger.info( f'{hkl[0]:4.0f} {hkl[1]:.0f} {hkl[2]:.0f} {hkl[3]:.0f} ' f'{hkl[4]:.0f} {hkl[5]:.6f}') return self._hkls_unique, self._ds_unique
[docs] @staticmethod def make_material( material_name, material_file=None, sgnum=None, lattice_parameters_angstroms=None, atoms=None, pos=None, dmin_angstroms=0.35): """Use `HeXRD <https://github.com/HEXRD/hexrd>`__ to get material properties when a materials file is provided. Use `xrayutilities <https://github.com/dkriegner/xrayutilities>`__ otherwise. :param material_name: Material name. :type material_name: str :param material_file: Material file name in cif or hdf5 format. :type material_file: str, optional :param sgnum: Space group number. :type sgnum: int, optional :param lattice_parameters_angstroms: Lattice parameters in angstrom. :type lattice_parameters_angstroms: float, optional :param atoms: Elements. :type atoms: list, optional :param pos: Wyckoff atom positions in the unit cell. :type pos: list, optional :param dmin_angstroms: Minimum lattice spacing in angstrom, defaults to `0.35`. :type dmin_angstroms: float, optional :return: Material properties. :rtype: hexrd.material.Material or xrayutilities.materials.Crystal """ # pylint: disable=possibly-used-before-assignment lattice_parameters = None if not isinstance(material_name, str): raise ValueError( f'Illegal material_name: {material_name} ' f'{type(material_name)}') if lattice_parameters_angstroms is not None: if material_file is not None: logger.warning( 'Overwrite lattice_parameters of material_file with input ' f'values ({lattice_parameters_angstroms})') raise NotImplementedError('material_file needs testing') if isinstance(lattice_parameters_angstroms, (int, float)): lattice_parameters = [lattice_parameters_angstroms] elif isinstance( lattice_parameters_angstroms, (tuple, list, np.ndarray)): lattice_parameters = list(lattice_parameters_angstroms) else: raise ValueError( 'Illegal lattice_parameters_angstroms: ' f'{lattice_parameters_angstroms} ' f'{type(lattice_parameters_angstroms)}') # FIX use "old" method for now (moved from CHAP/edd/utils.py) # Do not instantial with material name, it is ignored when you # do not also provide a material file matl = material.Material() matl.name = material_name matl.sgnum = sgnum if isinstance(lattice_parameters, float): lattice_parameters = [lattice_parameters] matl.latticeParameters = lattice_parameters matl.dmin = valWUnit('lp', 'length', dmin_angstroms, 'angstrom') nhkls = len(matl.planeData.exclusions) matl.planeData.set_exclusions(np.zeros(nhkls, dtype=bool)) return matl if material_file is None: if pos is not None: raise NotImplementedError(f'pos {type(pos)}: {pos}') if not isinstance(sgnum, int): raise ValueError(f'Illegal sgnum: {sgnum} {type(sgnum)}') if (sgnum is None or lattice_parameters_angstroms is None or pos is None): raise ValueError( 'Valid inputs for sgnum, lattice_parameters_angstroms and ' 'pos are required if materials file is not specified' f' {sgnum} {lattice_parameters_angstroms} {pos}') if isinstance(pos, str): pos = [pos] use_xu = True if (np.array(pos).ndim == 1 and isinstance(pos[0], (int, float)) and np.array(pos).size == 3): if HAVE_HEXRD: pos = np.array([pos]) use_xu = False elif (np.array(pos).ndim == 2 and np.array(pos).shape[0] > 0 and np.array(pos).shape[1] == 3): if HAVE_HEXRD: pos = np.array(pos) use_xu = False elif not (np.array(pos).ndim == 1 and isinstance(pos[0], str) and np.array(pos).size > 0 and HAVE_XU): raise ValueError( f'Illegal pos (HAVE_XU = {HAVE_XU}): {pos} {type(pos)}') if use_xu: if atoms is None: atoms = [material_name] matl = materials.Crystal( material_name, materials.SGLattice(sgnum, *lattice_parameters, atoms=atoms, pos=list(np.array(pos)))) else: # Do not instantial with material name, it is ignored # when you do not also provide a material file matl = material.Material() matl.name = material_name matl.sgnum = sgnum #matl.atominfo = np.vstack((pos.T, np.ones(pos.shape[0]))).T matl.latticeParameters = lattice_parameters matl.dmin = valWUnit( 'lp', 'length', dmin_angstroms, 'angstrom') exclusions = matl.planeData.get_exclusions() powder_intensity = matl.planeData.powder_intensity exclusions = [ exclusion or i >= len(powder_intensity) or powder_intensity[i] < POWDER_INTENSITY_CUTOFF for i, exclusion in enumerate(exclusions)] matl.planeData.set_exclusions(exclusions) logger.debug( f'powder_intensity = {matl.planeData.powder_intensity}') logger.debug(f'exclusions = {matl.planeData.exclusions}') else: if not HAVE_HEXRD: raise ValueError( 'Illegal inputs: must provide detailed material info when ' 'hexrd package is unavailable') if sgnum is not None: logger.warning( 'Ignore sgnum input when material_file is specified') if not (path.splitext(material_file)[1] in ('.h5', '.hdf5', '.xtal', '.cif')): raise ValueError(f'Illegal material file {material_file}') matl = material.Material( material_name, material_file, dmin=valWUnit('lp', 'length', dmin_angstroms, 'angstrom')) if lattice_parameters_angstroms is not None: matl.latticeParameters = lattice_parameters exclusions = matl.planeData.get_exclusions() powder_intensity = matl.planeData.powder_intensity exclusions = [ exclusion or i >= len(powder_intensity) or powder_intensity[i] < POWDER_INTENSITY_CUTOFF for i, exclusion in enumerate(exclusions)] matl.planeData.set_exclusions(exclusions) logger.debug( f'powder_intensity = {matl.planeData.powder_intensity}') logger.debug(f'exclusions = {matl.planeData.exclusions}') return matl