Source code for hexrd.wppf.amorphous

import numpy as np
import warnings
from scipy.interpolate import CubicSpline
from scipy.ndimage import gaussian_filter
from hexrd.wppf.peakfunctions import (
    _split_unit_gaussian as sp_gauss,
    _split_unit_pv as sp_pv)


AMORPHOUS_MODEL_TYPES = {
    'Split Gaussian': 'split_gaussian',
    'Split Pseudo-Voigt': 'split_pv',
    'Experimental': 'experimental',
}


[docs]class Amorphous: ''' >> @AUTHOR: Saransh Singh, Lawrence Livermore National Lab, saransh1@llnl.gov >> @DATE: 06/09/2025 SS 1.0 original >> @DETAILS: amorphous class can be used to include a broad, diffuse signal in the Rietveld refinement. The primary purpose is to extract an approximate solid/liquid phase fraction. This is best used for solid and liquid coming from materials with the same chemistry. The technique is most similar to degree of crystallinity (DOC) as documented in Dinnebier and Kern, "Quantification of amorphous phases - theory", PPXRD-13 workshop, Quantitative phase analysis by XRPD (2015) Attributes ---------- model_type : str allowed model types are "split_pv" for split pseudo-voight "split_gaussian" for split gaussian and "experimental" for an experimentally measured lineoiut of the amorphous phase model_data: numpy.ndarray if the "experimental model type is used, then model_data is a numpy array containing the 2theta-intensity of the measured amorphous signal. the signal in model_data will be shifted and scaled to get the best fit with the observed data." scale: if model is "experimental", then this quantifies the scale factor. otherwise, not present shift: if model is "experimental", then this quantifies the shift in 2theta. otherwise, not present smoothing: if model is "experimental", then this specifies how much (if any) gaussian smoothing to apply to the lineout ''' def __init__(self, tth_list, model_type='split_gaussian', model_data=None, scale=None, shift=None, smoothing=0, center=None, fwhm=None ): ''' Parameters ---------- tth_list: numpy.ndarray list of two-theta values for which amorphous intensity is computed model_type: str type of model to use for amorphous peak. this could be a predefined model such as "split_pv", or an experimentally measured pattern "experimental" model_data: numpy.ndarray, optional if the model is "experimental", then this optional array input is used as the model for amporphous peak. this model will be shifted and scaled to minimize the difference between observed and calculated intensities scale: dict scaling factor for the experimentally measured signal shift: float shift in two-theta for the experimental signal to match the observations smoothing: int width of gaussian kernel smoothing function center: dict center of split gaussian or pseudo-voight function. should have same keys as scale fwhm: dict dictionary of arrays of shape [2,] with [fwhm_l, fwhm_r] of the two halves for gaussian peak. for pseudo-voight peaks, shape is [4,] with entries for [fwhm_g_l, fwhm_l_l, fwhm_g_r, fwhm_l_r]. should have same keys as scale ''' if scale is None: scale = {'c1':1.} if shift is None: shift = {'c1':0.} if center is None: center = {'c1': 30.} if fwhm is None: if model_type == 'split_pv': array = np.array([5, 5, 5, 5]) else: array = np.array([5, 5]) fwhm = {'c1': array} self.tth_list = tth_list self.model_type = model_type self.model_data = model_data self.scale = scale self._shift = shift self._smoothing = smoothing self._center = center self._fwhm = fwhm @property def model_type(self): return self._model_type @model_type.setter def model_type(self, mtype): if mtype.lower() in ["split_pv", "split_gaussian", "experimental"]: self._model_type = mtype else: msg = (f'{mtype} is an unknown model type') raise ValueError(msg) @property def tth_list(self): return self._tth_list @tth_list.setter def tth_list(self, val): if isinstance(val, np.ma.MaskedArray): self._tth_list = val.filled() elif isinstance(val, np.ndarray): self._tth_list = val elif isinstance(val, (list, tuple)): self._tth_list = np.array(val) else: msg = f'{type(val)} not supported for tth_list' raise ValueError(msg) @property def model_data(self): return self._model_data @model_data.setter def model_data(self, data): if self.model_type.lower() == "experimental": if data is not None: if isinstance(data, dict): '''the liquid diffraction data might be on a different grid size and shape than the lineout. we will deal with that here via interpolation ''' data_interp = dict.fromkeys(data.keys()) for k, v in data.items(): if v.ndim == 2: # potential case with different tth step size mi = np.nanmin(v[:,1]) data_interp[k] = np.interp(self.tth_list, v[:, 0], v[:, 1], left=mi, right=mi) elif v.ndim == 1: data_interp[k] = v.copy() self._model_data = data_interp else: msg = f'data should be passed as a dictionary' raise ValueError(msg) else: msg = (f'experimental model is being used. ' f'please supply the data array') raise ValueError(msg) else: if data is not None: msg = (f'model data supplied will be ignored' f'for model type {self.model_type}') warnings.warn(msg) @property def scale(self): return self._scale @scale.setter def scale(self, val): if isinstance(val, dict): self._scale = val else: msg = f'scale should be passed as a dictionary' raise ValueError(msg) @property def shift(self): if self.model_type == "experimental": return self._shift return None @shift.setter def shift(self, val): if self.model_type == "experimental": if isinstance(val, dict): self._shift = val else: msg = f'shift should be passed as a dictionary' raise ValueError(msg) else: msg = (f'can not set shift for ' f'model_type {self.model_type}') warnings.warn(msg) @property def smoothing(self): if self.model_type == "experimental": return self._smoothing return None @smoothing.setter def smoothing(self, val): if self.model_type == "experimental": self._smoothing = val else: msg = (f'can not set smoothing for ' f'model_type {self.model_type}') warnings.warn(msg) @property def center(self): if self.model_type in ["split_gaussian", "split_pv"]: return self._center return None @center.setter def center(self, val): if self.model_type in ["split_gaussian", "split_pv"]: if isinstance(val, dict): self._center = val else: msg = f'center should be passed as a dictionary' raise ValueError(msg) else: msg = (f'can not set center for ' f'model_type {self.model_type}') warnings.warn(msg) @property def fwhm(self): if self.model_type in ["split_gaussian", "split_pv"]: return self._fwhm return None @fwhm.setter def fwhm(self, val): if self.model_type in ["split_gaussian", "split_pv"]: if isinstance(val, dict): sizes = np.array([val[k].size for k in val]) if self.model_type == "split_gaussian": if np.all(sizes==2): self._fwhm = val elif self.model_type == "split_pv": if np.all(sizes==4): self._fwhm = val else: msg = (f'incompatible fwhm size') else: msg = f'fwhm should be passed as a dictionary' raise ValueError(msg) else: msg = (f'can not set fwhm for ' f'model_type {self.model_type}') warnings.warn(msg) @property def amorphous_lineout(self): if self.model_type == "experimental": lo = np.zeros_like(self.tth_list) for key in self.shift: smooth_model_data = gaussian_filter( self.model_data[key], self.smoothing ) mi = np.nanmin(smooth_model_data) lo += self.scale[key]*np.interp( self.tth_list, self.tth_list+self.shift[key], smooth_model_data-mi, left=0., right=0.) + mi elif self.model_type in ["split_gaussian", "split_pv"]: lo = np.zeros_like(self.tth_list) for key in self.center: p = np.hstack((self.center[key], self.fwhm[key])) lo += self.scale[key]*self.peak_model(p, self.tth_list) return lo @property def integrated_area(self): x = self.tth_list y = self.amorphous_lineout return np.trapz(y, x) @property def peak_model(self): if self.model_type == "split_gaussian": return sp_gauss elif self.model_type == "split_pv": return sp_pv