Source code for pynucastro.rates.derived_rate

import math
import warnings
from collections import Counter

import numpy as np

from pynucastro.constants import constants
from pynucastro.nucdata import Nucleus
from pynucastro.rates.modified_rate import ModifiedRate
from pynucastro.rates.rate import Rate
from pynucastro.rates.reaclib_rate import ReacLibRate, SingleSet
from pynucastro.rates.tabular_rate import TabularRate


[docs] class DerivedRate(ReacLibRate): """A reverse rate computed from a forward rate via detailed balance. Parameters ---------- rate : Rate The forward rate that will be used to derive the reverse compute_Q : bool Do we recompute the Q-value of the rate from the masses? use_pf : bool Do we apply the partition function? """ def __init__(self, rate, compute_Q=False, use_pf=False): self.use_pf = use_pf self.rate = rate self.compute_Q = compute_Q if not isinstance(rate, Rate): raise TypeError('rate must be a Rate subclass') if (isinstance(rate, TabularRate) or self.rate.weak or self.rate.reverse): raise ValueError('The rate is reverse or weak or tabular') if not all(nuc.spin_states for nuc in self.rate.reactants): raise ValueError('One of the reactants spin ground state, is not defined') if not all(nuc.spin_states for nuc in self.rate.products): raise ValueError('One of the products spin ground state, is not defined') derived_sets = [] # The original rate of the modified rate is assumed to be ReacLibRate # Note: if dealing with ModifiedRate, the actual number of counts for # reactants and products come from rates.reactants and rates.product. # In the case of stoichiometry, this only affects the full ydot. # So it doesn't affect detailed balance calculation. if isinstance(rate, ModifiedRate): r = self.rate.original_rate else: r = self.rate Q = self.rate.Q if self.compute_Q: Q = 0.0 for n in set(self.rate.reactants): c = self.rate.reactant_count(n) Q += c * n.A_nuc for n in set(self.rate.products): c = self.rate.product_count(n) Q += -c * n.A_nuc Q *= constants.m_u_MeV_C18 prefactor = np.log(constants.m_u_C18) * (len(self.rate.reactants) - len(self.rate.products)) for nucr in self.rate.reactants: prefactor += 2.5*np.log(nucr.A_nuc) - np.log(nucr.A) + np.log(nucr.spin_states) for nucp in self.rate.products: prefactor += -2.5*np.log(nucp.A_nuc) + np.log(nucp.A) - np.log(nucp.spin_states) prefactor += np.log(self.counter_factors()[1]) - np.log(self.counter_factors()[0]) if len(self.rate.reactants) != len(self.rate.products): F = (constants.m_u_C18 * constants.k * 1.0e9 / (2.0*np.pi*constants.hbar**2))**(1.5*(len(self.rate.reactants) - len(self.rate.products))) prefactor += np.log(F) for ssets in r.sets: a = ssets.a a_rev = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] a_rev[0] = prefactor + a[0] a_rev[1] = a[1] - Q / (1.0e9 * constants.k_MeV) a_rev[2] = a[2] a_rev[3] = a[3] a_rev[4] = a[4] a_rev[5] = a[5] a_rev[6] = a[6] + 1.5*(len(self.rate.reactants) - len(self.rate.products)) sset_d = SingleSet(a=a_rev, labelprops=r.labelprops) derived_sets.append(sset_d) super().__init__(rfile=r.rfile, chapter=r.chapter, original_source=r.original_source, reactants=self.rate.products, products=self.rate.reactants, sets=derived_sets, labelprops="derived", Q=-Q) # explicitly mark it as reverse self.reverse = True # Update stoichiometry so that we are consistent in full ydot eqns self.stoichiometry = self.rate.stoichiometry self._set_print_representation() def _warn_about_missing_pf_tables(self): skip_nuclei = {Nucleus("h1"), Nucleus("n"), Nucleus("he4")} for nuc in set(self.rate.reactants + self.rate.products) - skip_nuclei: if not nuc.partition_function: warnings.warn(UserWarning(f'{nuc} partition function is not supported by tables: set pf = 1.0 by default'))
[docs] def eval(self, T, *, rho=None, comp=None): """Evaluate the derived reverse rate. Parameters ---------- T : float the temperature to evaluate the rate at rho : float the density to evaluate the rate at comp : Composition the composition to evaluate the rate with Returns ------- float """ r = super().eval(T=T, rho=rho, comp=comp) z_r = 1.0 z_p = 1.0 if self.use_pf: self._warn_about_missing_pf_tables() for nucr in self.rate.reactants: if not nucr.partition_function: continue #nucr.partition_function = lambda T: 1.0 z_r *= nucr.partition_function.eval(T) for nucp in self.rate.products: if not nucp.partition_function: continue #nucp.partition_function = lambda T: 1.0 z_p *= nucp.partition_function.eval(T) return r*z_r/z_p return r
[docs] def function_string_py(self): """Return a string containing the python function that computes the rate. Returns ------- str """ self._warn_about_missing_pf_tables() fstring = super().function_string_py() if self.use_pf: fstring += "\n" for nuc in set(self.rate.reactants + self.rate.products): if nuc.partition_function: fstring += f" # interpolating {nuc} partition function\n" fstring += f" {nuc}_pf_exponent = np.interp(tf.T9, xp={nuc}_temp_array, fp=np.log10({nuc}_pf_array))\n" fstring += f" {nuc}_pf = 10.0**{nuc}_pf_exponent\n" else: fstring += f" # setting {nuc} partition function to 1.0 by default, independent of T\n" fstring += f" {nuc}_pf = 1.0\n" fstring += "\n" fstring += " " fstring += "z_r = " fstring += "*".join([f"{nucr}_pf" for nucr in self.rate.reactants]) fstring += "\n" fstring += " " fstring += "z_p = " fstring += "*".join([f"{nucp}_pf" for nucp in self.rate.products]) fstring += "\n" fstring += f" rate_eval.{self.fname} *= z_r/z_p\n" return fstring
[docs] def function_string_cxx(self, dtype="double", specifiers="inline", leave_open=False, extra_args=None): """Return a string containing the C++ function that computes the derived reverse rate Parameters ---------- dtype : str The C++ datatype to use for all declarations specifiers : str C++ specifiers to add before each function declaration (i.e. "inline") leave_open : bool If ``true``, then we leave the function unclosed (no "}" at the end). This can allow additional functions to add to this output. extra_args : list(str) A list of strings representing additional arguments that should be appended to the argument list when defining the function interface. Returns ------- str """ if extra_args is None: extra_args = () self._warn_about_missing_pf_tables() extra_args = ["[[maybe_unused]] part_fun::pf_cache_t& pf_cache", *extra_args] fstring = super().function_string_cxx(dtype=dtype, specifiers=specifiers, leave_open=True, extra_args=extra_args) # right now we have rate and drate_dT without the partition # function now the partition function corrections if self.use_pf: fstring += "\n" for nuc in set(self.rate.reactants + self.rate.products): fstring += f" {dtype} {nuc}_pf, d{nuc}_pf_dT;\n" if nuc.partition_function: fstring += f" // interpolating {nuc} partition function\n" fstring += f" get_partition_function_cached({nuc.cindex()}, tfactors, pf_cache, {nuc}_pf, d{nuc}_pf_dT);\n" else: fstring += f" // setting {nuc} partition function to 1.0 by default, independent of T\n" fstring += f" {nuc}_pf = 1.0_rt;\n" fstring += f" d{nuc}_pf_dT = 0.0_rt;\n" fstring += "\n" fstring += f" {dtype} z_r = " fstring += " * ".join([f"{nucr}_pf" for nucr in self.rate.reactants]) fstring += ";\n" fstring += f" {dtype} z_p = " fstring += " * ".join([f"{nucp}_pf" for nucp in self.rate.products]) fstring += ";\n\n" # now the derivatives, via chain rule chain_terms = [] for n in self.rate.reactants: chain_terms.append(" * ".join([f"{nucr}_pf" for nucr in self.rate.reactants if nucr != n] + [f"d{n}_pf_dT"])) fstring += f" {dtype} dz_r_dT = " fstring += " + ".join(chain_terms) fstring += ";\n" chain_terms = [] for n in self.rate.products: chain_terms.append(" * ".join([f"{nucp}_pf" for nucp in self.rate.products if nucp != n] + [f"d{n}_pf_dT"])) fstring += f" {dtype} dz_p_dT = " fstring += " + ".join(chain_terms) fstring += ";\n\n" fstring += f" {dtype} dzterm_dT = (z_p * dz_r_dT - z_r * dz_p_dT) / (z_p * z_p);\n\n" # final terms fstring += " drate_dT = dzterm_dT * rate + drate_dT * (z_r / z_p);\n" fstring += " rate *= z_r/z_p;\n\n" if not leave_open: fstring += "}\n\n" return fstring
[docs] def counter_factors(self): """Compute the multiplicity factor, nucr! = nucr_1! * ... * nucr_r!, for each repeated nucr reactant and nucp! = nucp_1! * ... * nucp_p! for each nucp product in a ordered pair (nucr!, nucp!). The factors nucr! and nucp! avoid overcounting when more than one nuclei is involve in the reaction. If there is no multiplicity, then the factor is 1.0. Returns ------- tuple(float, float) """ react_counts = Counter(self.rate.reactants) prod_counts = Counter(self.rate.products) reactant_factor = 1.0 for nuc in set(self.rate.reactants): reactant_factor *= math.factorial(react_counts[nuc]) product_factor = 1.0 for nuc in set(self.rate.products): product_factor *= math.factorial(prod_counts[nuc]) return (reactant_factor, product_factor)