Source code for pynucastro.networks.sympy_network_support

"""Support functions for interpreting the rates, ydots, and Jacobian
through SymPy.

"""

import re

import sympy

from pynucastro.rates import TabularRate


[docs] class SympyRates: """A collection of rates stored as SymPy objects.""" def __init__(self): self.symbol_ludict = {} # Symbol lookup dictionary self._ydot_term_cache = {} self.name_density = 'state.rho' self.name_electron_fraction = 'state.y_e' # Define these for the particular network self.name_rate_data = 'screened_rates' self.name_y = 'Y' self.name_ydot = 'ydot' self.name_ydot_nuc = 'ydot_nuc' self.name_jacobian = 'jac' self.name_jacobian_nuc = 'jac' self.symbol_ludict['__dens__'] = self.name_density self.symbol_ludict['__y_e__'] = self.name_electron_fraction self.float_explicit_num_digits = 17
[docs] def ydot_term_symbol(self, rate, y_i): """Construct a sympy expression containing this rate's contribution to the ydot term for nuclide y_i. Parameters ---------- rate : Rate the reaction rate we are working with y_i : Nucleus the nucleus for which we want to construct the dY/dt term corresponding to ``rate``. Returns ------- sympy.core.expr.Expr """ key = (rate.cname(), y_i) if key in self._ydot_term_cache: return self._ydot_term_cache[key] srate = self.specific_rate_symbol(rate) # Check if y_i is a reactant or product c_reac = rate.reactant_count(y_i) c_prod = rate.product_count(y_i) if c_reac == 0 and c_prod == 0: # The rate doesn't contribute to the ydot for this y_i ydot_sym = float(sympy.sympify(0.0)) else: # y_i appears as a product or reactant ydot_sym = (c_prod - c_reac) * srate result = ydot_sym.evalf(n=self.float_explicit_num_digits) self._ydot_term_cache[key] = result return result
[docs] def specific_rate_symbol(self, rate): """Construct a SymPy expression containing this rate's term in a dY/dt equation, e.g. ρ Y(A)Y(B) <σv> / (1 + ẟ_AB) for a rate A + B Also enter the symbol and substitution in the lookup table. Parameters ---------- rate : Rate the reaction rate to consider Returns ------- sympy.core.expr.Expr """ # composition dependence Y_sym = 1 for r in sorted(set(rate.reactants)): c = rate.reactants.count(r) sym_final = f'{self.name_y}({r.cindex()})' sym_temp = f'Y__j{r}__' self.symbol_ludict[sym_temp] = sym_final Y_sym = Y_sym * sympy.symbols(sym_temp)**c # density dependence dens_sym = sympy.symbols('__dens__')**rate.dens_exp # electron fraction if electron capture reaction y_e_sym = sympy.sympify(1) if not isinstance(rate, TabularRate): if rate.weak_type == 'electron_capture' and not rate.tabular: y_e_sym = sympy.symbols('__y_e__') # prefactor prefactor_sym = sympy.sympify(1)/sympy.sympify(rate.inv_prefactor) # screened rate sym_final = self.name_rate_data + f'(k_{rate.cname()})' sym_temp = f'NRD__k_{rate.cname()}__' self.symbol_ludict[sym_temp] = sym_final screened_rate_sym = sympy.symbols(sym_temp) srate_sym = prefactor_sym * dens_sym * y_e_sym * Y_sym * screened_rate_sym return srate_sym
[docs] def jacobian_term_symbol(self, rate, ydot_j, y_i): """Construct a SymPy expression containing a single rate's contribution to the Jacobian matrix element d(dY_j/dt)/dY_i. We return both the SymPy expression and a bool indicating whether the term is null. Parameters ---------- rate : Rate the rate we are considering ydot_j : Nucleus the evolution equation, dY_j/dt we are working on y_i : Nucleus the nucleus we are differentiating with respect to. Returns ------- tuple(sympy.core.expr.Expr, bool) """ ydot_sym = self.ydot_term_symbol(rate, ydot_j) deriv_sym = sympy.symbols(f'Y__j{y_i}__') jac_sym = sympy.diff(ydot_sym, deriv_sym) symbol_is_null = False if jac_sym.equals(0): symbol_is_null = True return (jac_sym.evalf(n=self.float_explicit_num_digits), symbol_is_null)
[docs] def cxxify(self, s): """Given string s, generated from a SymPy expression, replace the placeholder symbols with the values maintained in the `symbol_ludict` dictionary. Parameters ---------- s : str the input string Returns ------- str """ for k, v in self.symbol_ludict.items(): s = s.replace(k, v) if s == '0': s = '0.0e0' # Replace all double precision literals with custom real type # literals # constant type specifier const_spec = "_rt" # we want append any "e" scientific notation with "_rt". This # matches stuff like -1.25d-10, and gives us separate groups # for the prefix and exponent. The [^\w] makes sure a letter # isn't right in front of the match (like # 'k3d-1'). Alternately, we allow for a match at the start of # the string. e_re = re.compile(r"([^\w\+\-]|\A)([\+\-0-9.][0-9.]+)[eE]([\+\-]?[0-9]+)", re.IGNORECASE | re.DOTALL) # update "d" scientific notation -- allow for multiple # constants in a single string for ee in e_re.finditer(s): old_num = ee.group(0).strip() s = s.replace(old_num, f"{old_num}{const_spec}") return s