Module tau2.rates_calculator
Functions
def calculate_detailed_balance(gamma, energies, T)-
Expand source code
def calculate_detailed_balance(gamma, energies, T): """ Calculates the detailed balance check for a given rate matrix. Args: gamma (np.ndarray): The (N, N) transition rate matrix. gamma[f, i] is the rate from state i to state f. energies (np.ndarray): A (N,) array of state energies in cm^-1. T (float): The temperature in Kelvin. Returns: tuple: - det_balance_matrix (np.ndarray): The (N, N) matrix of absolute differences. - detailed_value_sum (float): The scalar sum of all unique pair differences, used for the check. """ # Suppress division by zero warnings for calculating the rate ratio with np.errstate(divide='ignore', invalid='ignore'): # Calculate the Boltzmann factor for all pairs (f, i) # energy_diffs[f, i] = E_i - E_f energy_diffs = energies - energies[:, np.newaxis] # Clip the argument to np.exp to avoid overflow, which happens for large # positive energy differences (large E_i, small E_f) at low T. # A value of 700 is safe, as exp(709) is near the float64 limit. clipped_arg = np.clip(energy_diffs / (KB_INV_CM * T), a_min=None, a_max=700) boltzmann_matrix = np.exp(clipped_arg) # Calculate the ratio of forward (i->f) to reverse (f->i) rates # gamma_ratio[f, i] = gamma[f, i] / gamma[i, f] gamma_ratio = np.divide(gamma, gamma.T, out=np.full_like(gamma, np.inf), where=(gamma.T != 0)) # Calculate the absolute difference from the expected Boltzmann ratio det_balance_matrix = np.abs(gamma_ratio - boltzmann_matrix) # Correct for the case where both forward and reverse rates are zero # In this case, the ratio is effectively 1 both_zero_mask = (gamma == 0.0) & (gamma.T == 0.0) det_balance_matrix[both_zero_mask] = np.abs(1.0 - boltzmann_matrix[both_zero_mask]) # The detailed balance value is the sum of differences for all unique # pairs (i, f). We sum the lower triangle (where f > i) to get this. detailed_value_sum = np.sum(np.tril(det_balance_matrix, k=-1)) # Check if the total sum exceeds the tolerance if detailed_value_sum > 1: print(f"!!! WARNING !!!: Detailed balance not obeyed at {T} K. Sum = {detailed_value_sum}") return det_balance_matrix, detailed_value_sumCalculates the detailed balance check for a given rate matrix.
Args
gamma:np.ndarray- The (N, N) transition rate matrix. gamma[f, i] is the rate from state i to state f.
energies:np.ndarray- A (N,) array of state energies in cm^-1.
T:float- The temperature in Kelvin.
Returns
tuple: - det_balance_matrix (np.ndarray): The (N, N) matrix of absolute differences. - detailed_value_sum (float): The scalar sum of all unique pair differences, used for the check.
def calculate_rates_from_data(data, temperatures, max_state, rate_integrator='trapz')-
Expand source code
def calculate_rates_from_data(data, temperatures, max_state, rate_integrator='trapz'): """ Calculates relaxation rates from pre-computed Raman spectral density data. This function takes the spectral density integrands (J(omega)) for various Raman processes and integrates them over phonon energy (omega) to obtain the transition rates (Gamma_if). It then constructs the full rate matrix (Gamma) and extracts the final relaxation rate from its eigenvalues. Args: data (dict): A dictionary containing: - 'omega_grid' (np.ndarray): The phonon energy grid (cm^-1). - 'energies' (np.ndarray): Zeeman-split electronic energies (cm^-1). - 'integrands' (dict): A nested dictionary where keys are (i, f) transition tuples and values are dictionaries of process integrands ('pp', 'mm', 'pm', 'mp'). temperatures (list or np.ndarray): List of temperatures (K) at which to calculate the rates. max_state (int): The maximum electronic state index to consider for the Gamma matrix (states 0 to max_state-1). rate_integrator (str, optional): The numerical integration method. Supports 'trapz' (default) and 'simpson'. Returns: tuple: A tuple containing: - np.ndarray: An array of final relaxation rates (in s^-1) for each temperature (typically the second smallest eigenvalue). - dict: A dictionary of the full Gamma matrices for each temperature, with temperatures as keys. """ omega_grid = data['omega_grid'] energies = data['energies'] # 1. Integrate spectral densities to get rates for each T all_transition_rates = {} # Filter transitions based on keys present in data['integrands'] # We assume 'data' contains only the transitions requested. transitions = list(data['integrands'].keys()) # Identify unique states involved in the transitions to map them to matrix indices involved_states = sorted(list(set([idx for pair in transitions for idx in pair]))) # Create a mapping from state index -> matrix index (0, 1, 2...) # If the user requested states [2, 3], involved_states is [2, 3]. # map: 2->0, 3->1. state_to_matrix_idx = {state_idx: i for i, state_idx in enumerate(involved_states)} matrix_dim = max(max_state, len(involved_states)) for (i, f), integrands_dict in data['integrands'].items(): delta_E = energies[f] - energies[i] rates_T = [] # --- Direct Integration Methods --- for T in temperatures: if T <= 0: rates_T.append(0.0) continue n1 = get_thermal_pop(omega_grid, T) temp_dep_integrand = np.zeros_like(omega_grid) # Term 'pp' (emission-emission) if 'pp' in integrands_dict: omega_2 = -delta_E - omega_grid n2 = get_thermal_pop(omega_2, T) thermal_factor = (n1 + 1.0) * (n2 + 1.0) temp_dep_integrand += integrands_dict['pp'] * thermal_factor # Term 'mm' (absorption-absorption) if 'mm' in integrands_dict: omega_2 = delta_E - omega_grid n2 = get_thermal_pop(omega_2, T) thermal_factor = n1 * n2 temp_dep_integrand += integrands_dict['mm'] * thermal_factor # Term 'pm' (absorption-emission) if 'pm' in integrands_dict: omega_2 = delta_E + omega_grid n2 = get_thermal_pop(omega_2, T) thermal_factor = (n1 + 1.0) * n2 temp_dep_integrand += integrands_dict['pm'] * thermal_factor # Term 'mp' (emission-absorption) if 'mp' in integrands_dict: omega_2 = -delta_E + omega_grid n2 = get_thermal_pop(omega_2, T) thermal_factor = n1 * (n2 + 1.0) temp_dep_integrand += integrands_dict['mp'] * thermal_factor if rate_integrator == 'trapz': total_integral = trapz(temp_dep_integrand, omega_grid) elif rate_integrator == 'simpson': total_integral = simpson(temp_dep_integrand, x=omega_grid) else: sys.exit(f"Error: rate_integrator '{rate_integrator}' is not supported. " "Only 'trapz' and 'simpson' are currently implemented.") rates_T.append(total_integral * TWO_PI_OVER_HBAR) all_transition_rates[(i, f)] = np.array(rates_T) rate_values = [] gamma_matrices = {} det_balance_matrices = {} det_values = {} for t_idx, T in enumerate(temperatures): gamma = np.zeros((matrix_dim, matrix_dim)) for (i, f), rates_at_temps in all_transition_rates.items(): # Use mapped indices if available, otherwise original indices # If state indices (e.g. 2, 3) are outside matrix_dim (e.g. 2), map them. idx_i = state_to_matrix_idx.get(i, i) idx_f = state_to_matrix_idx.get(f, f) # Safety check to keep within bounds if idx_i < matrix_dim and idx_f < matrix_dim: gamma[idx_f, idx_i] = rates_at_temps[t_idx] np.fill_diagonal(gamma, -np.sum(gamma, axis=0)) gamma_matrices[T] = gamma eigs = np.sort(np.abs(np.real(np.linalg.eigvals(gamma)))) # The first eigenvalue is always 0 (equilibrium). The second is the relaxation rate. rate_val = eigs[1] if len(eigs) > 1 else 0.0 rate_values.append(rate_val) # Detailed balance check requires energies for the specific states in the matrix # Map indices back to original energy indices matrix_energies = np.zeros(matrix_dim) for state_idx, matrix_idx in state_to_matrix_idx.items(): if matrix_idx < matrix_dim: matrix_energies[matrix_idx] = energies[state_idx] det_balance_matrices[T], det_values[T] = calculate_detailed_balance(gamma, matrix_energies, T) return np.array(rate_values), gamma_matrices, det_balance_matricesCalculates relaxation rates from pre-computed Raman spectral density data.
This function takes the spectral density integrands (J(omega)) for various Raman processes and integrates them over phonon energy (omega) to obtain the transition rates (Gamma_if). It then constructs the full rate matrix (Gamma) and extracts the final relaxation rate from its eigenvalues.
Args
data:dict- A dictionary containing: - 'omega_grid' (np.ndarray): The phonon energy grid (cm^-1). - 'energies' (np.ndarray): Zeeman-split electronic energies (cm^-1). - 'integrands' (dict): A nested dictionary where keys are (i, f) transition tuples and values are dictionaries of process integrands ('pp', 'mm', 'pm', 'mp').
temperatures:listornp.ndarray- List of temperatures (K) at which to calculate the rates.
max_state:int- The maximum electronic state index to consider for the Gamma matrix (states 0 to max_state-1).
rate_integrator:str, optional- The numerical integration method. Supports 'trapz' (default) and 'simpson'.
Returns
tuple- A tuple containing: - np.ndarray: An array of final relaxation rates (in s^-1) for each temperature (typically the second smallest eigenvalue). - dict: A dictionary of the full Gamma matrices for each temperature, with temperatures as keys.
def get_thermal_pop(omega, T, omega_cutoff=0.001)-
Expand source code
def get_thermal_pop(omega, T, omega_cutoff=0.001): """ Computes the Bose-Einstein thermal population factor for phonons. This function calculates n(omega) = 1 / (exp(omega / (kB*T)) - 1), handling potential overflow for very large omega/T values and ensuring correct behavior for omega <= 0. Args: omega (float or np.ndarray): Phonon energy (in cm^-1). T (float): Temperature (in Kelvin). omega_cutoff (float, optional): A small positive value to avoid singularity when omega is very close to zero. Defaults to 0.001 cm^-1. Returns: float or np.ndarray: The Bose-Einstein thermal population factor(s). """ is_scalar = np.isscalar(omega) omega_arr = np.atleast_1d(omega) # Default result is 0. This is the correct limit for omega <= 0 # and for the high-energy/low-temp case that causes overflow. pop = np.zeros_like(omega_arr, dtype=float) # Only perform calculations for positive energies. mask = omega_arr > 0 if np.any(mask): # Apply cutoff to avoid singularity near zero. omega_pos = np.maximum(omega_arr[mask], omega_cutoff) arg = omega_pos / (KB_INV_CM * T) # Temporarily ignore benign overflow warnings. # The result of 1.0 / np.expm1(inf) is correctly 0.0. with np.errstate(over='ignore'): pop[mask] = 1.0 / np.expm1(arg) return pop.item() if is_scalar else popComputes the Bose-Einstein thermal population factor for phonons.
This function calculates n(omega) = 1 / (exp(omega / (kB*T)) - 1), handling potential overflow for very large omega/T values and ensuring correct behavior for omega <= 0.
Args
omega:floatornp.ndarray- Phonon energy (in cm^-1).
T:float- Temperature (in Kelvin).
omega_cutoff:float, optional- A small positive value to avoid singularity when omega is very close to zero. Defaults to 0.001 cm^-1.
Returns
floatornp.ndarray- The Bose-Einstein thermal population factor(s).