Source code for TRXASprefitpack.driver.ads

'''
ads:
submodule for driver routine of associated difference spectrum

:copyright: 2021-2022 by pistack (Junho Lee).
:license: LGPL3.
'''

from typing import Optional, Tuple
import numpy as np
from scipy.linalg import svd, lstsq
from ..mathfun.A_matrix import make_A_matrix_dmp_osc, make_A_matrix_exp
from ..mathfun.rate_eq import compute_signal_irf


[docs]def dads(escan_time: np.ndarray, fwhm: float, tau: np.ndarray, base: Optional[bool] = True, irf: Optional[str] = 'g', eta: Optional[float] = None, intensity: Optional[np.ndarray] = None, eps: Optional[np.ndarray] = None) \ -> Tuple[np.ndarray, np.ndarray, np.ndarray]: ''' Calculate decay associated difference spectrum from experimental energy scan data Args: escan_time: time delay for each energy scan data fwhm: full width at half maximum of instrumental response function tau: life time for each component base: whether or not include baseline [default: True] irf: shape of instrumental response function [default: g] * 'g': normalized gaussian distribution, * 'c': normalized cauchy distribution, * 'pv': pseudo voigt profile :math:`(1-\\eta)g(t, {fwhm}) + \\eta c(t, {fwhm})` eta: mixing parameter for pseudo voigt profile (only needed for pseudo voigt profile) intensity: intensity of energy scan dataset eps: standard error of dataset Returns: Tuple of calculated decay associated difference spectrum of each component, estimated error, and retrieved energy scan intensity from dads and decay components Note: To calculate decay associated difference spectrum of n component exponential decay, you should measure at least n+1 energy scan ''' # initialization if base: c = np.empty((tau.size+1, intensity.shape[0])) dof = escan_time.size - (tau.size+1) else: c = np.empty((tau.size, intensity.shape[0])) dof = escan_time.size - (tau.size) if eps is None: eps = np.ones_like(intensity) c_eps = np.empty_like(c) A = make_A_matrix_exp(escan_time, fwhm, tau, base, irf, eta) data_scaled = intensity/eps # evaluates dads cond = 1e-2 for i in range(intensity.shape[0]): A_scaled = np.einsum('j,ij->ij', 1/eps[i, :], A) U, s, Vh = svd(A_scaled.T, full_matrices=False) mask = s > cond*s[0] U_turn = U[:, mask] s_turn = s[mask] Vh_turn = Vh[mask, :] c[:, i] = np.einsum('j,ij->ij', 1/s_turn, Vh_turn.T) @ (U_turn.T @ data_scaled[i, :]) res = data_scaled[i, :] - (c[:, i] @ A_scaled) if dof != 0: red_chi2 = np.sum(res**2)/dof cov_scale = red_chi2 *\ Vh_turn.T @ np.einsum('i,ij->ij', 1/s_turn**2, Vh_turn) c_eps[:, i] = np.sqrt(np.diag(cov_scale)) return c, c_eps, c.T @ A
[docs]def dads_svd(escan_time: np.ndarray, fwhm: float, tau: np.ndarray, base: Optional[bool] = True, irf: Optional[str] = 'g', eta: Optional[float] = None, intensity: Optional[np.ndarray] = None, cond_num: Optional[float] = 0) \ -> Tuple[np.ndarray, np.ndarray]: ''' Calculate decay associated difference spectrum from experimental energy scan data (using svd) Args: escan_time: time delay for each energy scan data fwhm: full width at half maximum of instrumental response function tau: life time for each component base: whether or not include baseline [default: True] irf: shape of instrumental response function [default: g] * 'g': normalized gaussian distribution, * 'c': normalized cauchy distribution, * 'pv': pseudo voigt profile :math:`(1-\\eta)g(t, {fwhm}) + \\eta c(t, {fwhm})` eta: mixing parameter for pseudo voigt profile (only needed for pseudo voigt profile) intensity: intensity of energy scan dataset cond_num: conditional number to turncate svd Returns: Tuple of calculated decay associated difference spectrum of each component, and retrieved energy scan intensity from dads and decay components Note: To calculate decay associated difference spectrum of n component exponential decay, you should measure at least n energy scan ''' # svd of data matrix U_data, s_data, Vh_data = svd(intensity, full_matrices=False) N_survived = np.sum((s_data > cond_num*s_data[0])) U_data_trun = U_data[:, :N_survived] s_data_trun = s_data[:N_survived] Vh_turn = Vh_data[:N_survived, :] A = make_A_matrix_exp(escan_time, fwhm, tau, base, irf, eta) c, _, _, _ = lstsq(A.T, Vh_turn.T, cond=1e-2) coeff = np.einsum('j,ij->ij', s_data_trun, U_data_trun) @ c.T fit = coeff @ A return coeff, fit
def dads_osc(escan_time: np.ndarray, fwhm: float, tau: np.ndarray, tau_osc: np.ndarray, period_osc: np.ndarray, base: Optional[bool] = True, irf: Optional[str] = 'g', eta: Optional[float] = None, intensity: Optional[np.ndarray] = None, eps: Optional[np.ndarray] = None) \ -> Tuple[np.ndarray, np.ndarray, np.ndarray]: ''' Calculate decay associated difference spectrum with damped oscillation from experimental energy scan data Args: escan_time: time delay for each energy scan data fwhm: full width at half maximum of instrumental response function tau: life time for each component tau_osc: life time for each oscillation component period_osc: period of each oscillation base: whether or not include baseline [default: True] irf: shape of instrumental response function [default: g] * 'g': normalized gaussian distribution, * 'c': normalized cauchy distribution, * 'pv': pseudo voigt profile :math:`(1-\\eta)g(t, {fwhm}) + \\eta c(t, {fwhm})` eta: mixing parameter for pseudo voigt profile (only needed for pseudo voigt profile) intensity: intensity of energy scan dataset eps: standard error of dataset Returns: 1. decay associated differenece spectrum 2. damped oscillation associated difference spectrum 3. estimated error of dads 4. estimated error of doads 5. retrieved energy scan from dads 6. retrieved energy scan from doads Note: 1. To calculate decay associated difference spectrum of n component of exponential decay and m component of damped oscillation, you should measure at least :math:`n+2m+1` energy scan 2. 0 to m-1 raw of doads is doads of cosine component and m to :math:`2m-1` raw of doads is doads of sine component ''' # initialization if base: c = np.empty((tau.size+1+2*tau_osc.size, intensity.shape[0])) A = np.empty((tau.size+2*tau_osc.size+1, escan_time.size)) dof = escan_time.size - (tau.size+1+2*tau_osc.size) idx_osc = tau.size+1 else: c = np.empty((tau.size+2*tau_osc.size, intensity.shape[0])) A = np.empty((tau.size+2*tau_osc.size, escan_time.size)) dof = escan_time.size - (tau.size+2*tau_osc.size) idx_osc = tau.size if eps is None: eps = np.ones_like(intensity) c_eps = np.empty_like(c) A[:idx_osc, :] = make_A_matrix_exp(escan_time, fwhm, tau, base, irf, eta) A[idx_osc:, :] = make_A_matrix_dmp_osc(escan_time, fwhm, tau_osc, period_osc, irf, eta) data_scaled = intensity/eps # evaluates dads cond = 1e-2 for i in range(intensity.shape[0]): A_scaled = np.einsum('j,ij->ij', 1/eps[i, :], A) U, s, Vh = svd(A_scaled.T, full_matrices=False) mask = s > cond*s[0] U_turn = U[:, mask] s_turn = s[mask] Vh_turn = Vh[mask, :] c[:, i] = np.einsum('j,ij->ij', 1/s_turn, Vh_turn.T) @ (U_turn.T @ data_scaled[i, :]) res = data_scaled[i, :] - (c[:, i] @ A_scaled) if dof != 0: red_chi2 = np.sum(res**2)/dof cov_scale = red_chi2 *\ Vh_turn.T @ np.einsum('i,ij->ij', 1/s_turn**2, Vh_turn) c_eps[:, i] = np.sqrt(np.diag(cov_scale)) return c[:idx_osc,:], c[idx_osc:, :], \ c_eps[:idx_osc, :], c_eps[idx_osc:, :], \ c[:idx_osc, :].T @ A[:idx_osc, :], \ c[idx_osc:, :].T @ A[idx_osc:, :]
[docs]def sads(escan_time: np.ndarray, fwhm: float, eigval: np.ndarray, V: np.ndarray, c: np.ndarray, exclude: Optional[str] = None, irf: Optional[str] = 'g', eta: Optional[float] = None, intensity: Optional[np.ndarray] = None, eps: Optional[np.ndarray] = None) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: ''' Calculate species associated difference spectrum from experimental energy scan data Args: escan_time: time delay for each energy scan data fwhm: full width at half maximum of instrumental response function eigval: eigenvalue of rate equation matrix V: eigenvector of rate equation matrix c: coefficient to match initial condition of rate equation exclude: exclude either 'first' or 'last' element or both 'first' and 'last' element. * 'first' : exclude first element * 'last' : exclude last element * 'first_and_last' : exclude both first and last element * None : Do not exclude any element [default] irf: shape of instrumental response function [default: g] * 'g': normalized gaussian distribution, * 'c': normalized cauchy distribution, * 'pv': pseudo voigt profile :math:`(1-\\eta)g(t, {fwhm}) + \\eta c(t, {fwhm})` eta: mixing parameter for pseudo voigt profile (only needed for pseudo voigt profile) intensity: intensity of energy scan dataset eps: standard error of data Returns: Tuple of calculated species associated difference spectrum of each component, estimated error and retrieved intensity of energy scan from sads and model excited state components Note: 1. eigval, V, c should be obtained from solve_model 2. To calculate species associated difference spectrum of n excited state species, you should measure at least n+1 energy scan 3. Difference spectrum of ground state is zero, so ground state species should be excluded from rate equation or via exclude option. ''' # initialization if exclude is None: diff_abs = np.empty((eigval.size, intensity.shape[0])) dof = escan_time.size - eigval.size elif exclude in ['first', 'last']: diff_abs = np.empty((eigval.size-1, intensity.shape[0])) dof = escan_time.size - (eigval.size-1) else: diff_abs = np.empty((eigval.size-2, intensity.shape[0])) dof = escan_time.size - (eigval.size-2) if eps is None: eps = np.ones_like(intensity) diff_abs_eps = np.empty_like(diff_abs) A = compute_signal_irf(escan_time, eigval, V, c, fwhm, irf, eta) if exclude == 'first': B = A[1:, :] elif exclude == 'last': B = A[:-1, :] elif exclude == 'first_and_last': B = A[1:-1, :] else: B = A data_scaled = intensity/eps # evaluates sads cond = 1e-2 for i in range(intensity.shape[0]): A_scaled = np.einsum('j,ij->ij', 1/eps[i, :], B) U, s, Vh = svd(A_scaled.T, full_matrices=False) mask = s > cond*s[0] U_turn = U[:, mask] s_turn = s[mask] Vh_turn = Vh[mask, :] cov = Vh_turn.T @ np.einsum('i,ij->ij', 1/s_turn**2, Vh_turn) diff_abs[:, i] = np.einsum('j,ij->ij', 1/s_turn, Vh_turn.T) @ (U_turn.T @ data_scaled[i, :]) res = data_scaled[i, :] - (diff_abs[:, i] @ A_scaled) if dof != 0: red_chi2 = np.sum(res**2)/dof diff_abs_eps[:, i] = np.sqrt(red_chi2*np.diag(cov)) return diff_abs, diff_abs_eps, diff_abs.T @ B
[docs]def sads_svd(escan_time: np.ndarray, fwhm: float, eigval: np.ndarray, V: np.ndarray, c: np.ndarray, exclude: Optional[str] = None, irf: Optional[str] = 'g', eta: Optional[float] = None, intensity: Optional[np.ndarray] = None, cond_num: Optional[float] = 0) -> Tuple[np.ndarray, np.ndarray]: ''' Calculate species associated difference spectrum from experimental energy scan data (using svd) Args: escan_time: time delay for each energy scan data fwhm: full width at half maximum of instrumental response function eigval: eigenvalue of rate equation matrix V: eigenvector of rate equation matrix c: coefficient to match initial condition of rate equation exclude: exclude either 'first' or 'last' element or both 'first' and 'last' element. * 'first' : exclude first element * 'last' : exclude last element * 'first_and_last' : exclude both first and last element * None : Do not exclude any element [default] irf: shape of instrumental response function [default: g] * 'g': normalized gaussian distribution, * 'c': normalized cauchy distribution, * 'pv': pseudo voigt profile :math:`(1-\\eta)g(t, {fwhm}) + \\eta c(t, {fwhm})` eta: mixing parameter for pseudo voigt profile (only needed for pseudo voigt profile) intensity: intensity of energy scan dataset cond_num: conditional number to turncate svd Returns: Tuple of calculated species associated difference spectrum of each component, and retrieved intensity of energy scan from sads and model excited state components Note: 1. eigval, V, c should be obtained from solve_model 2. To calculate species associated difference spectrum of n excited state species, you should measure at least n+1 energy scan 3. Difference spectrum of ground state is zero, so ground state species should be excluded from rate equation or via exclude option. ''' # svd U_data, s_data, Vh_data = svd(intensity, full_matrices=False) N_survived = np.sum((s_data > cond_num*s_data[0])) U_data_trun = U_data[:, :N_survived] s_data_trun = s_data[:N_survived] Vh_turn = Vh_data[:N_survived, :] A = compute_signal_irf(escan_time, eigval, V, c, fwhm, irf, eta) if exclude == 'first': B = A[1:, :] elif exclude == 'last': B = A[:-1, :] elif exclude == 'first_and_last': B = A[1:-1, :] else: B = A c, _, _, _ = lstsq(B.T, Vh_turn.T, cond=1e-2) coeff = np.einsum('j,ij->ij', s_data_trun, U_data_trun) @ c.T fit = coeff @ B return coeff, fit
def sads_osc(escan_time: np.ndarray, fwhm: float, eigval: np.ndarray, V: np.ndarray, c: np.ndarray, tau_osc: np.ndarray, period_osc: np.ndarray, exclude: Optional[str] = None, irf: Optional[str] = 'g', eta: Optional[float] = None, intensity: Optional[np.ndarray] = None, eps: Optional[np.ndarray] = None) \ -> Tuple[np.ndarray, np.ndarray, np.ndarray]: ''' Calculate species associated difference spectrum with damped oscillation from experimental energy scan data Args: escan_time: time delay for each energy scan data fwhm: full width at half maximum of instrumental response function eigval: eigenvalue of rate equation matrix V: eigenvector of rate equation matrix c: coefficient to match initial condition of rate equation tau_osc: life time for each oscillation component period_osc: period of each oscillation exclude: exclude either 'first' or 'last' element or both 'first' and 'last' element. * 'first' : exclude first element * 'last' : exclude last element * 'first_and_last' : exclude both first and last element * None : Do not exclude any element [default] irf: shape of instrumental response function [default: g] * 'g': normalized gaussian distribution, * 'c': normalized cauchy distribution, * 'pv': pseudo voigt profile :math:`(1-\\eta)g(t, {fwhm}) + \\eta c(t, {fwhm})` eta: mixing parameter for pseudo voigt profile (only needed for pseudo voigt profile) intensity: intensity of energy scan dataset eps: standard error of data Returns: 1. species associated difference spectrum 2. damped oscillation asscoiated difference spectrum 3. estimated error of sads 4. estimated error of doads 5. retrieved energy scan from sads 6. retrieved energy scan from doads Note: 1. eigval, V, c should be obtained from solve_model 2. To calculate species associated difference spectrum of n excited state species with m damped oscillation component, you should measure at least :math:`n+2m+1` energy scan 3. Difference spectrum of ground state is zero, so ground state species should be excluded from rate equation or via exclude option. 4. 0 to m-1 raw of doads is doads of cosine component and m to :math:`2m-1` raw of doads is doads of sine component ''' # initialization if exclude is None: B = np.empty((eigval.size+2*tau_osc.size, escan_time.size)) diff_abs = np.empty((eigval.size+2*tau_osc.size, intensity.shape[0])) dof = escan_time.size - (eigval.size+2*tau_osc.size) idx_osc = eigval.size elif exclude in ['first', 'last']: B = np.empty((eigval.size-1+2*tau_osc.size, escan_time.size)) diff_abs = np.empty((eigval.size-1+2*tau_osc.size, intensity.shape[0])) dof = escan_time.size - (eigval.size-1+2*tau_osc.size) idx_osc = eigval.size-1 else: B = np.empty((eigval.size-2+2*tau_osc.size, escan_time.size)) diff_abs = np.empty((eigval.size-2, intensity.shape[0])) dof = escan_time.size - (eigval.size-2+2*tau_osc.size) idx_osc = eigval.size-2 if eps is None: eps = np.ones_like(intensity) diff_abs_eps = np.empty_like(diff_abs) A = compute_signal_irf(escan_time, eigval, V, c, fwhm, irf, eta) if exclude == 'first': B[:idx_osc, :] = A[1:, :] elif exclude == 'last': B[:idx_osc, :] = A[:-1, :] elif exclude == 'first_and_last': B[:idx_osc, :] = A[1:-1, :] else: B[:idx_osc, :] = A B[idx_osc:, :] = make_A_matrix_dmp_osc(escan_time, fwhm, tau_osc, period_osc, irf, eta) data_scaled = intensity/eps # evaluates sads cond = 1e-2 for i in range(intensity.shape[0]): A_scaled = np.einsum('j,ij->ij', 1/eps[i, :], B) U, s, Vh = svd(A_scaled.T, full_matrices=False) mask = s > cond*s[0] U_turn = U[:, mask] s_turn = s[mask] Vh_turn = Vh[mask, :] cov = Vh_turn.T @ np.einsum('i,ij->ij', 1/s_turn**2, Vh_turn) diff_abs[:, i] = np.einsum('j,ij->ij', 1/s_turn, Vh_turn.T) @ (U_turn.T @ data_scaled[i, :]) res = data_scaled[i, :] - (diff_abs[:, i] @ A_scaled) if dof != 0: red_chi2 = np.sum(res**2)/dof diff_abs_eps[:, i] = np.sqrt(red_chi2*np.diag(cov)) return diff_abs[:idx_osc, :], diff_abs[idx_osc:, :], \ diff_abs_eps[:idx_osc, :], diff_abs_eps[idx_osc:, :], \ diff_abs[:idx_osc, :].T @ B[:idx_osc, :], \ diff_abs[idx_osc:, :].T @ B[idx_osc:, :]