diff --git a/docs/physics/plasma/equilibrium/tardis_solver_cmfgen.ipynb b/docs/physics/plasma/equilibrium/tardis_solver_cmfgen.ipynb new file mode 100644 index 00000000000..08cfc941560 --- /dev/null +++ b/docs/physics/plasma/equilibrium/tardis_solver_cmfgen.ipynb @@ -0,0 +1,254 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Rates using CMFGEN data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import astropy.units as u\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "from tardis.io.atom_data import AtomData\n", + "from tardis.io.configuration.config_reader import Configuration\n", + "from tardis.model.base import SimulationState\n", + "from tardis.plasma.equilibrium.rates import (\n", + " AnalyticPhotoionizationCoeffSolver,\n", + " EstimatedPhotoionizationCoeffSolver,\n", + " RadiativeRatesSolver,\n", + " SpontaneousRecombinationCoeffSolver,\n", + " ThermalCollisionalRateSolver,\n", + ")\n", + "from tardis.plasma.radiation_field import (\n", + " DilutePlanckianRadiationField,\n", + ")\n", + "\n", + "home = str(Path('~').expanduser())\n", + "\n", + "config = Configuration.from_yaml(home+\"/tardis/tardis/plasma/tests/data/plasma_base_test_config.yml\")\n", + "\n", + "config.model.structure.velocity.num = 5\n", + "\n", + "ion_slice = (1, 0, slice(None), slice(None))\n", + "\n", + "\n", + "def get_radiative_rate_solver(radiative_transitions):\n", + " rad_rate_solver = RadiativeRatesSolver(radiative_transitions)\n", + " return rad_rate_solver\n", + "\n", + "def get_chianti_collisional_rate_solver(atom_data, radiative_transitions,):\n", + " col_strength_temperatures = atom_data.collision_data_temperatures\n", + " col_strengths = atom_data.collision_data.loc[ion_slice, :]\n", + " collisional_rate_solver = ThermalCollisionalRateSolver(atom_data.levels, radiative_transitions, col_strength_temperatures, col_strengths, 'chianti', \"none\")\n", + " return collisional_rate_solver\n", + "\n", + "def get_cmfgen_collisional_rate_solver(atom_data, radiative_transitions,):\n", + " col_strength_temperatures = atom_data.yg_data.columns\n", + " col_strengths = atom_data.yg_data.loc[ion_slice, :]\n", + " collisional_rate_solver = ThermalCollisionalRateSolver(atom_data.levels, radiative_transitions, col_strength_temperatures, col_strengths, 'cmfgen', \"regemorter\")\n", + " return collisional_rate_solver\n", + "\n", + "cmfgen_atom_data = AtomData.from_hdf(home+'/carsus/docs/kurucz_cd23_cmfgen_H_Ti.h5')\n", + "\n", + "cmfgen_radiative_transitions = cmfgen_atom_data.lines.loc[ion_slice, :]\n", + "\n", + "cmfgen_sim_state = SimulationState.from_config(config, atom_data=cmfgen_atom_data)\n", + "\n", + "cmfgen_atom_data.prepare_atom_data([1], \"macroatom\", [], [(1, 0)])" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from tardis.plasma.electron_energy_distribution import (\n", + " ThermalElectronEnergyDistribution,\n", + ")\n", + "\n", + "cmfgen_radiative_rate_solver = get_radiative_rate_solver(cmfgen_radiative_transitions)\n", + "\n", + "cmfgen_collisional_rate_solver = get_cmfgen_collisional_rate_solver(cmfgen_atom_data, cmfgen_radiative_transitions)\n", + "\n", + "temperature = cmfgen_sim_state.t_radiative\n", + "\n", + "# need a better way to make a custom radiation field where the intensity is zero\n", + "# in specific locations as desired\n", + "rad_field = DilutePlanckianRadiationField(cmfgen_sim_state.t_radiative, dilution_factor=np.ones_like(cmfgen_sim_state.t_radiative) * 0.5)\n", + "electron_dist = ThermalElectronEnergyDistribution(0, cmfgen_sim_state.t_radiative, 1e6 * u.g/u.cm**3)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "rate_solvers = [(cmfgen_radiative_rate_solver, \"radiative\"), (cmfgen_collisional_rate_solver, \"electron\")]\n", + "\n", + "lte_rate_solvers = [(cmfgen_collisional_rate_solver, \"electron\")]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "rad_rates = cmfgen_radiative_rate_solver.solve(rad_field)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "col_rates = cmfgen_collisional_rate_solver.solve(electron_dist.temperature)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "photoionization_estimator = pd.read_csv(\"photoionization_rate.csv\", index_col=[0, 1, 2])\n", + "\n", + "stimulated_recombination_estimator = pd.read_csv(\"stimulated_recombination_rate.csv\", index_col=[0, 1, 2])\n", + "\n", + "class Estimators:\n", + " def __init__(self, photoionization_estimator, stimulated_recombination_estimator):\n", + " self.photo_ion_estimator = photoionization_estimator\n", + " self.stim_recomb_estimator = stimulated_recombination_estimator\n", + "\n", + "estimators = Estimators(photoionization_estimator, stimulated_recombination_estimator)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "spontaneous_recombination_rate_solver = SpontaneousRecombinationCoeffSolver(cmfgen_atom_data.photoionization_data)\n", + "photoionization_rate_solver = AnalyticPhotoionizationCoeffSolver(cmfgen_atom_data.photoionization_data)\n", + "\n", + "spontaneous_recombination_rate = spontaneous_recombination_rate_solver.solve(temperature.value)\n", + "photoionization_rate, stimulated_recombination_rate = photoionization_rate_solver.solve(rad_field, temperature.value)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "estimated_photoion_rate_solver = EstimatedPhotoionizationCoeffSolver(cmfgen_atom_data.level2continuum_edge_idx)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "est_photoionization_rate, est_stimulated_recombination_rate = estimated_photoion_rate_solver.solve(estimators, cmfgen_sim_state.time_explosion, cmfgen_sim_state.geometry.volume)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "photoionization_rate.plot()\n", + "est_photoionization_rate.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from tardis.plasma.equilibrium.level_populations import LevelPopulationSolver\n", + "from tardis.plasma.equilibrium.rate_matrix import RateMatrix\n", + "\n", + "rate_matrix_solver = RateMatrix(rate_solvers, cmfgen_atom_data.levels)\n", + "\n", + "rate_matrix = rate_matrix_solver.solve(rad_field, electron_dist)\n", + "\n", + "lte_rate_matrix = RateMatrix(lte_rate_solvers, cmfgen_atom_data.levels).solve(rad_field, electron_dist)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "solver = LevelPopulationSolver(rate_matrix, cmfgen_atom_data.levels)\n", + "\n", + "level_pops = solver.solve()\n", + "\n", + "lte_level_pops = LevelPopulationSolver(lte_rate_matrix, cmfgen_atom_data.levels).solve()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.scatter(cmfgen_atom_data.levels.loc[1,0].energy * u.erg.to('eV'), level_pops.loc[1,0,:][0], marker='x', label='TARDIS')\n", + "plt.scatter(cmfgen_atom_data.levels.loc[1,0].energy * u.erg.to('eV'), lte_level_pops.loc[1,0,:][0], marker='x', label='TARDIS col only')\n", + "plt.xlabel(\"Energy (eV)\")\n", + "plt.ylabel(\"Population\")\n", + "plt.semilogy()\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "tardis", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tardis/plasma/equilibrium/rates/__init__.py b/tardis/plasma/equilibrium/rates/__init__.py index 2d7e4b79bc2..1b2d08593ae 100644 --- a/tardis/plasma/equilibrium/rates/__init__.py +++ b/tardis/plasma/equilibrium/rates/__init__.py @@ -2,9 +2,20 @@ UpsilonCMFGENSolver, UpsilonRegemorterSolver, ) +from tardis.plasma.equilibrium.rates.collisional_ionization_rates import ( + CollisionalIonizationSolver, +) +from tardis.plasma.equilibrium.rates.collisional_ionization_strengths import ( + CollisionalIonizationSeaton, +) from tardis.plasma.equilibrium.rates.collisional_rates import ( ThermalCollisionalRateSolver, ) +from tardis.plasma.equilibrium.rates.photoionization_strengths import ( + AnalyticPhotoionizationCoeffSolver, + EstimatedPhotoionizationCoeffSolver, + SpontaneousRecombinationCoeffSolver, +) from tardis.plasma.equilibrium.rates.radiative_rates import ( RadiativeRatesSolver, ) diff --git a/tardis/plasma/equilibrium/rates/collisional_ionization_rates.py b/tardis/plasma/equilibrium/rates/collisional_ionization_rates.py new file mode 100644 index 00000000000..084fb5ebbc8 --- /dev/null +++ b/tardis/plasma/equilibrium/rates/collisional_ionization_rates.py @@ -0,0 +1,50 @@ +from tardis.plasma.equilibrium.rates.collisional_ionization_strengths import ( + CollisionalIonizationSeaton, +) + + +class CollisionalIonizationSolver: + """Solver for collisional ionization and recombination rates.""" + + def __init__(self, photoionization_cross_sections): + self.photoionization_cross_sections = photoionization_cross_sections + + def solve(self, electron_temperature, saha_factor, approximation="seaton"): + """Solve the collisional ionization and recombination rates. + + Parameters + ---------- + electron_temperature : u.Quantity + Electron temperatures per cell + saha_factor : pandas.DataFrame, dtype float + The Saha factor for each cell. Indexed by atom number, ion number, level number. + approximation : str, optional + The rate approximation to use, by default "seaton" + + Returns + ------- + pd.DataFrame + Collisional ionization rates + pd.DataFrame + Collisional recombination rates + + Raises + ------ + ValueError + If an unsupported approximation is requested. + """ + if approximation == "seaton": + strength_solver = CollisionalIonizationSeaton( + self.photoionization_cross_sections, electron_temperature + ) + else: + raise ValueError(f"approximation {approximation} not supported") + + collision_ionization_rates = strength_solver.solve() + + # Inverse of the ionization rate for equilibrium + collision_recombination_rates = collision_ionization_rates.multiply( + saha_factor.loc[collision_ionization_rates.index] + ) + + return collision_ionization_rates, collision_recombination_rates diff --git a/tardis/plasma/equilibrium/rates/collisional_ionization_strengths.py b/tardis/plasma/equilibrium/rates/collisional_ionization_strengths.py new file mode 100644 index 00000000000..eb37720e023 --- /dev/null +++ b/tardis/plasma/equilibrium/rates/collisional_ionization_strengths.py @@ -0,0 +1,56 @@ +import numpy as np +import pandas as pd + +from tardis import constants as const + +H = const.h.cgs.value +K_B = const.k_B.cgs.value + + +class CollisionalIonizationSeaton: + """Solver for collisional ionization rates in the Seaton approximation.""" + + def __init__(self, photoionization_cross_sections): + self.photoionization_cross_sections = photoionization_cross_sections + + def solve(self, electron_temperature): + """ + Parameters + ---------- + electron_temperature : u.Quantity + The electron temperature in K. + + Returns + ------- + pandas.DataFrame, dtype float + The rate coefficient for collisional ionization in the Seaton + approximation. Multiply with the electron density and the + level number density to obtain the total rate. + + Notes + ----- + The rate coefficient for collisional ionization in the Seaton approximation + is calculated according to Eq. 9.60 in [1]. + + References + ---------- + .. [1] Hubeny, I. and Mihalas, D., "Theory of Stellar Atmospheres". 2014. + """ + photo_ion_cross_sections_threshold = ( + self.photoionization_cross_sections.groupby(level=[0, 1, 2]).first() + ) + nu_i = photo_ion_cross_sections_threshold["nu"] + u0s = nu_i.values[np.newaxis].T / electron_temperature * (H / K_B) + factor = np.exp(-u0s) / u0s + factor = pd.DataFrame(factor, index=nu_i.index) + coll_ion_coeff = 1.55e13 * photo_ion_cross_sections_threshold["x_sect"] + coll_ion_coeff = factor.multiply(coll_ion_coeff, axis=0) + coll_ion_coeff = coll_ion_coeff.divide( + np.sqrt(electron_temperature), axis=1 + ) + + ion_number = coll_ion_coeff.index.get_level_values("ion_number").values + coll_ion_coeff[ion_number == 0] *= 0.1 + coll_ion_coeff[ion_number == 1] *= 0.2 + coll_ion_coeff[ion_number >= 2] *= 0.3 + return coll_ion_coeff diff --git a/tardis/plasma/equilibrium/rates/photoionization_rates.py b/tardis/plasma/equilibrium/rates/photoionization_rates.py new file mode 100644 index 00000000000..0caa433ef3d --- /dev/null +++ b/tardis/plasma/equilibrium/rates/photoionization_rates.py @@ -0,0 +1,61 @@ +from tardis.plasma.equilibrium.rates.photoionization_strengths import ( + AnalyticPhotoionizationCoeffSolver, + EstimatedPhotoionizationCoeffSolver, + SpontaneousRecombinationCoeffSolver, +) + + +class PhotoionizationRateSolver: + def __init__( + self, photoionization_cross_sections, level2continuum_edge_idx + ): + self.photoionization_cross_sections = photoionization_cross_sections + self.level2continuum_edge_idx = level2continuum_edge_idx + + def solve( + self, electron_temperature, n_i, n_k, n_e, saha_factor, type="analytic" + ): + if type == "analytic": + # TODO: try merging the analytic and estimator based approaches + # look at the Lucy 2003 equations and our MC estimator classes + photoionization_rate_coeff_solver = ( + AnalyticPhotoionizationCoeffSolver( + self.photoionization_cross_sections + ) + ) + elif type == "estimated": + photoionization_rate_coeff_solver = ( + EstimatedPhotoionizationCoeffSolver( + self.level2continuum_edge_idx + ) + ) + else: + raise ValueError(f"Type {type} not supported") + + spontaneous_recombination_rate_coeff_solver = ( + SpontaneousRecombinationCoeffSolver( + self.photoionization_cross_sections + ) + ) + + photoionization_rate_coeff, stimulated_recombination_rate_coeff = ( + # TODO: bifurcation of classes here is a problem + photoionization_rate_coeff_solver.solve() + ) + + spontaneous_recombination_rate_coeff = ( + spontaneous_recombination_rate_coeff_solver.solve( + electron_temperature + ) + ) + + # TODO: decide if these n_i, n_k, n_e numbers should be here (probably not) + photoionization_rate = ( + photoionization_rate_coeff * n_i + - saha_factor * stimulated_recombination_rate_coeff * n_k * n_e + ) + spontaneous_recombination_rate = ( + saha_factor * spontaneous_recombination_rate_coeff * n_k * n_e + ) + + return photoionization_rate, spontaneous_recombination_rate diff --git a/tardis/plasma/equilibrium/rates/photoionization_strengths.py b/tardis/plasma/equilibrium/rates/photoionization_strengths.py new file mode 100644 index 00000000000..4cea2aaa96f --- /dev/null +++ b/tardis/plasma/equilibrium/rates/photoionization_strengths.py @@ -0,0 +1,285 @@ +import numpy as np +import pandas as pd + +from tardis import constants as const +from tardis.transport.montecarlo.estimators.util import ( + bound_free_estimator_array2frame, + integrate_array_by_blocks, +) + +C = const.c.cgs.value +H = const.h.cgs.value +K_B = const.k_B.cgs.value + + +class SpontaneousRecombinationCoeffSolver: + def __init__( + self, + photoionization_cross_sections, + ): + self.photoionization_cross_sections = photoionization_cross_sections + self.nu = self.photoionization_cross_sections.nu.values + + @property + def common_prefactor(self): + return ( + 4.0 + * np.pi + * self.photoionization_cross_sections.x_sect + / (H * self.nu) + ) + + def calculate_photoionization_boltzmann_factor(self, electron_temperature): + return np.exp(-self.nu[np.newaxis].T / electron_temperature * (H / K_B)) + + def solve(self, electron_temperature): + """ + Calculate the spontaneous recombination rate coefficient. + + Parameters + ---------- + electron_temperature : u.Quantity + Electron temperature in each cell. + + Returns + ------- + pd.DataFrame + The calculated spontaneous recombination rate coefficient. + + Notes + ----- + Equation 13 in Lucy 2003. + """ + # need to fix array multiplication + prefactor = self.common_prefactor * (2 * H * self.nu**3.0) / (C**2.0) + photoionization_boltzmann_factor = pd.DataFrame( + self.calculate_photoionization_boltzmann_factor( + electron_temperature + ), + index=prefactor.index, + ) + spontaneous_recombination_rate_coeff = ( + photoionization_boltzmann_factor.multiply( + prefactor, + axis=0, + ) + ) + return spontaneous_recombination_rate_coeff + + +class AnalyticPhotoionizationCoeffSolver(SpontaneousRecombinationCoeffSolver): + def __init__( + self, + photoionization_cross_sections, + ): + super().__init__(photoionization_cross_sections) + + self.photoionization_block_references = np.pad( + self.photoionization_cross_sections.nu.groupby(level=[0, 1, 2]) + .count() + .values.cumsum(), + [1, 0], + ) + + self.photoionization_index = ( + self.photoionization_cross_sections.index.unique() + ) + + def calculate_mean_intensity_photoionization_df( + self, + dilute_blackbody_radiationfield_state, + ): + mean_intensity = ( + dilute_blackbody_radiationfield_state.calculate_mean_intensity( + self.nu + ) + ) + return pd.DataFrame( + mean_intensity, + index=self.photoionization_cross_sections.index, + columns=np.arange( + len(dilute_blackbody_radiationfield_state.temperature) + ), + ) + + def calculate_photoionization_rate_coeff( + self, + mean_intensity_photoionization_df, + ): + """ + Calculate the photoionization rate coefficient. + + Parameters + ---------- + dilute_blackbody_radiationfield_state : DiluteBlackBodyRadiationFieldState + A dilute black body radiation field state. + + Returns + ------- + pd.DataFrame + The calculated photoionization rate coefficient. + + Notes + ----- + Equation 16 in Lucy 2003. + """ + photoionization_rate_coeff = mean_intensity_photoionization_df.multiply( + self.common_prefactor, + axis=0, + ) + photoionization_rate_coeff = integrate_array_by_blocks( + photoionization_rate_coeff.values, + self.nu, + self.photoionization_block_references, + ) + photoionization_rate_coeff = pd.DataFrame( + photoionization_rate_coeff, + index=self.photoionization_index, + ) + return photoionization_rate_coeff + + def calculate_stimulated_recombination_rate_coeff( + self, + mean_intensity_photoionization_df, + photoionization_boltzmann_factor, + ): + """ + Calculate the photoionization rate coefficient. + + Parameters + ---------- + mean_intensity_photoionization_df : pd.DataFrame + Mean intensity at each photoionization frequency. + photoionization_boltzmann_factor : np.ndarray + Boltzmann factor for each photoionization frequency. + + Returns + ------- + pd.DataFrame + The stimulated recombination rate coefficient. + + Notes + ----- + Equation 15 in Lucy 2003. + """ + stimulated_recombination_rate_coeff = ( + mean_intensity_photoionization_df * photoionization_boltzmann_factor + ) + + stimulated_recombination_rate_coeff = ( + mean_intensity_photoionization_df.multiply( + self.common_prefactor, + axis=0, + ) + ) + stimulated_recombination_rate_coeff = integrate_array_by_blocks( + stimulated_recombination_rate_coeff.values, + self.nu, + self.photoionization_block_references, + ) + stimulated_recombination_rate_coeff = pd.DataFrame( + stimulated_recombination_rate_coeff, + index=self.photoionization_index, + ) + return stimulated_recombination_rate_coeff + + def solve( + self, + dilute_blackbody_radiationfield_state, + electron_temperature, + ): + """ + Prepares the ionization and recombination coefficients by grouping them for + ion numbers. + + Parameters + ---------- + dilute_blackbody_radiationfield_state : DiluteBlackBodyRadiationFieldState + The dilute black body radiation field state. + electron_temperature : u.Quantity + Electron temperature in each shell. + + Returns + ------- + photoionization_rate_coeff + Photoionization rate coefficient grouped by atomic number and ion number. + recombination_rate_coeff + Radiative recombination rate coefficient grouped by atomic number and ion number. + """ + photoionization_boltzmann_factor = ( + self.calculate_photoionization_boltzmann_factor( + electron_temperature + ) + ) + + mean_intensity_photoionization_df = ( + self.calculate_mean_intensity_photoionization_df( + dilute_blackbody_radiationfield_state + ) + ) + # Equation 16 Lucy 2003 + photoionization_rate_coeff = self.calculate_photoionization_rate_coeff( + mean_intensity_photoionization_df, + ) + # Equation 15 Lucy 2003. Must be multiplied by Saha LTE factor Phi_ik + stimulated_recombination_rate_coeff = ( + self.calculate_stimulated_recombination_rate_coeff( + mean_intensity_photoionization_df, + photoionization_boltzmann_factor, + ) + ) + + return ( + photoionization_rate_coeff, + stimulated_recombination_rate_coeff, + ) + + +class EstimatedPhotoionizationCoeffSolver: + def __init__( + self, + level2continuum_edge_idx, + ): + self.level2continuum_edge_idx = level2continuum_edge_idx + + def solve( + self, + radfield_mc_estimators, + time_simulation, + volume, + ): + """ + Solve for the continuum properties. + + Parameters + ---------- + radfield_mc_estimators : RadiationFieldMCEstimators + The Monte Carlo estimators for the radiation field. + time_simulation : float + The simulation time. + volume : float + The volume of the cells. + + Returns + ------- + ContinuumProperties + The calculated continuum properties. + """ + # TODO: the estimators are computed in the form epsilon_nu * distance * xsection / comoving_nu + # with the stimulated recombination multiplied by a Boltzmann factor exp(-h * comoving_nu / k * electron_temp) + # This is why this method does not match the one in AnalyticPhotoionizationCoeffSolver + photoionization_normalization = (time_simulation * volume * H) ** -1 + + photoionization_rate_coeff = bound_free_estimator_array2frame( + radfield_mc_estimators.photo_ion_estimator, + self.level2continuum_edge_idx, + ) + photoionization_rate_coeff *= photoionization_normalization + + stimulated_recombination_rate_coeff = bound_free_estimator_array2frame( + radfield_mc_estimators.stim_recomb_estimator, + self.level2continuum_edge_idx, + ) + stimulated_recombination_rate_coeff *= photoionization_normalization + + return photoionization_rate_coeff, stimulated_recombination_rate_coeff