def test_proton_electron_ratio(hdf5_dbase_root): t = np.logspace(4, 9, 100) * u.K # NOTE: this number will not be accurate as we are using only a subset of # the database pe_ratio = fiasco.proton_electron_ratio(t, hdf5_dbase_root=hdf5_dbase_root) assert type(pe_ratio) is u.Quantity assert pe_ratio.shape == t.shape
def contribution_function(self, density: u.cm**(-3), **kwargs): """ Contribution function :math:`G(n,T)` for all transitions The contribution function for ion :math:`k` of element :math:`X` for a particular transition :math:`ij` is given by, .. math:: G_{ij} = \\frac{n_H}{n_e}\mathrm{Ab}(X)f_{X,k}N_jA_{ij}\Delta E_{ij}\\frac{1}{n_e}, and has units erg :math:`\mathrm{cm}^{3}` :math:`\mathrm{s}^{-1}`. Note that the contribution function is often defined in differing ways by different authors. The contribution function is defined as above in [1]_. The corresponding wavelengths can be retrieved with, .. code-block:: python ion.transitions.wavelength[~ion.transitions.is_twophoton] Parameters ---------- density : `~astropy.units.Quantity` Electron number density References ---------- .. [1] Young, P. et al., 2016, J. Phys. B: At. Mol. Opt. Phys., `49, 7 <http://iopscience.iop.org/article/10.1088/0953-4075/49/7/074009/meta>`_ """ populations = self.level_populations(density, **kwargs) p2e = proton_electron_ratio(self.temperature, **self._dset_names) term = np.outer(p2e * self.ioneq, 1. / density.value) * self.abundance / density.unit # Exclude two-photon transitions upper_level = self.transitions.upper_level[~self.transitions. is_twophoton] # CHIANTI records theoretical transitions with negative wavelengths wavelength = np.fabs( self.transitions.wavelength[~self.transitions.is_twophoton]) A = self.transitions.A[~self.transitions.is_twophoton] energy = ((const.h * const.c) / wavelength).to(u.erg) i_upper = vectorize_where(self._elvlc['level'], upper_level) g = term[:, :, np.newaxis] * populations[:, :, i_upper] * (A * energy) return g
def level_populations(self, density: u.cm**(-3), include_protons=True): """ Compute energy level populations as a function of temperature and density Parameters ---------- density : `~astropy.units.Quantity` include_protons : `bool`, optional If True (default), include proton excitation and de-excitation rates """ level = self._elvlc['level'] lower_level = self._scups['lower_level'] upper_level = self._scups['upper_level'] coeff_matrix = np.zeros(self.temperature.shape + ( level.max(), level.max(), )) / u.s # Radiative decay out of current level coeff_matrix[:, level - 1, level - 1] -= vectorize_where_sum( self.transitions.upper_level, level, self.transitions.A.value) * self.transitions.A.unit # Radiative decay into current level from upper levels coeff_matrix[:, self.transitions.lower_level - 1, self.transitions.upper_level - 1] += (self.transitions.A) # Collisional--electrons ex_rate_e = self.electron_collision_excitation_rate() dex_rate_e = self.electron_collision_deexcitation_rate() ex_diagonal_e = vectorize_where_sum( lower_level, level, ex_rate_e.value.T, 0).T * ex_rate_e.unit dex_diagonal_e = vectorize_where_sum( upper_level, level, dex_rate_e.value.T, 0).T * dex_rate_e.unit # Collisional--protons if include_protons and self._psplups is not None: pe_ratio = proton_electron_ratio(self.temperature, **self._dset_names) proton_density = np.outer(pe_ratio, density)[:, :, np.newaxis] ex_rate_p = self.proton_collision_excitation_rate() dex_rate_p = self.proton_collision_deexcitation_rate() ex_diagonal_p = vectorize_where_sum(self._psplups['lower_level'], level, ex_rate_p.value.T, 0).T * ex_rate_p.unit dex_diagonal_p = vectorize_where_sum(self._psplups['upper_level'], level, dex_rate_p.value.T, 0).T * dex_rate_p.unit # Solve matrix equation for each density value populations = np.zeros(self.temperature.shape + density.shape + (level.max(), )) b = np.zeros(self.temperature.shape + (level.max(), )) b[:, -1] = 1.0 for i, d in enumerate(density): c_matrix = coeff_matrix.copy() # Collisional excitation and de-excitation out of current state c_matrix[:, level - 1, level - 1] -= d * (ex_diagonal_e + dex_diagonal_e) # De-excitation from upper states c_matrix[:, lower_level - 1, upper_level - 1] += d * dex_rate_e # Excitation from lower states c_matrix[:, upper_level - 1, lower_level - 1] += d * ex_rate_e # Same processes as above, but for protons if include_protons and self._psplups is not None: d_p = proton_density[:, i, :] c_matrix[:, level - 1, level - 1] -= d_p * (ex_diagonal_p + dex_diagonal_p) c_matrix[:, self._psplups['lower_level'] - 1, self._psplups['upper_level'] - 1] += (d_p * dex_rate_p) c_matrix[:, self._psplups['upper_level'] - 1, self._psplups['lower_level'] - 1] += (d_p * ex_rate_p) # Invert matrix c_matrix[:, -1, :] = 1. * c_matrix.unit pop = np.linalg.solve(c_matrix.value, b) pop = np.where(pop < 0., 0., pop) pop /= pop.sum(axis=1)[:, np.newaxis] populations[:, i, :] = pop return u.Quantity(populations)
def level_populations(self, density: u.cm**(-3), include_protons=True) -> u.dimensionless_unscaled: """ Energy level populations as a function of temperature and density. Parameters ---------- density : `~astropy.units.Quantity` include_protons : `bool`, optional If True (default), include proton excitation and de-excitation rates. Returns ------- `~astropy.units.Quantity` A ``(l, m, n)`` shaped quantity, where ``l`` is the number of temperatures, ``m`` is the number of densities, and ``n`` is the number of energy levels. """ # NOTE: Cannot include protons if psplups data not available try: _ = self._psplups except KeyError: # TODO: log this include_protons = False level = self._elvlc['level'] lower_level = self._scups['lower_level'] upper_level = self._scups['upper_level'] coeff_matrix = np.zeros(self.temperature.shape + (level.max(), level.max(),))/u.s # Radiative decay out of current level coeff_matrix[:, level-1, level-1] -= vectorize_where_sum( self.transitions.upper_level, level, self.transitions.A.value) * self.transitions.A.unit # Radiative decay into current level from upper levels coeff_matrix[:, self.transitions.lower_level-1, self.transitions.upper_level-1] += ( self.transitions.A) # Collisional--electrons dex_rate_e = self.electron_collision_deexcitation_rate() ex_rate_e = self.electron_collision_excitation_rate(deexcitation_rate=dex_rate_e) ex_diagonal_e = vectorize_where_sum( lower_level, level, ex_rate_e.value.T, 0).T * ex_rate_e.unit dex_diagonal_e = vectorize_where_sum( upper_level, level, dex_rate_e.value.T, 0).T * dex_rate_e.unit # Collisional--protons if include_protons: lower_level_p = self._psplups['lower_level'] upper_level_p = self._psplups['upper_level'] pe_ratio = proton_electron_ratio(self.temperature, **self._dset_names, hdf5_dbase_root=self.hdf5_dbase_root) proton_density = np.outer(pe_ratio, density)[:, :, np.newaxis] ex_rate_p = self.proton_collision_excitation_rate() dex_rate_p = self.proton_collision_deexcitation_rate(excitation_rate=ex_rate_p) ex_diagonal_p = vectorize_where_sum( lower_level_p, level, ex_rate_p.value.T, 0).T * ex_rate_p.unit dex_diagonal_p = vectorize_where_sum( upper_level_p, level, dex_rate_p.value.T, 0).T * dex_rate_p.unit # Populate density dependent terms and solve matrix equation for each density value density = np.atleast_1d(density) populations = np.zeros(self.temperature.shape + density.shape + (level.max(),)) for i, d in enumerate(density): c_matrix = coeff_matrix.copy() # Collisional excitation and de-excitation out of current state c_matrix[:, level-1, level-1] -= d*(ex_diagonal_e + dex_diagonal_e) # De-excitation from upper states c_matrix[:, lower_level-1, upper_level-1] += d*dex_rate_e # Excitation from lower states c_matrix[:, upper_level-1, lower_level-1] += d*ex_rate_e # Same processes as above, but for protons if include_protons and self._psplups is not None: d_p = proton_density[:, i, :] c_matrix[:, level-1, level-1] -= d_p*(ex_diagonal_p + dex_diagonal_p) c_matrix[:, lower_level_p-1, upper_level_p-1] += d_p * dex_rate_p c_matrix[:, upper_level_p-1, lower_level_p-1] += d_p * ex_rate_p # Invert matrix val, vec = np.linalg.eig(c_matrix.value) # Eigenvectors with eigenvalues closest to zero are the solutions to the homogeneous # system of linear equations # NOTE: Sometimes eigenvalues may have complex component due to numerical stability. # We will take only the real component as our rate matrix is purely real i_min = np.argmin(np.fabs(np.real(val)), axis=1) pop = np.take(np.real(vec), i_min, axis=2)[range(vec.shape[0]), :, range(vec.shape[0])] # NOTE: The eigenvectors can only be determined up to a sign so we must enforce # positivity np.fabs(pop, out=pop) np.divide(pop, pop.sum(axis=1)[:, np.newaxis], out=pop) populations[:, i, :] = pop return u.Quantity(populations)