Solving for nuclear statistical equilibrium

Solving for nuclear statistical equilibrium#

pynucastro is able to calculate the abundances at nuclear statistical equilibrium of a given reaction network.

The constrained equations are setup following Calder et al. (2007) and Seitenzahl et al. (2009) and solved using scipy.optimize.fsolve().

Here we show how to find the NSE state of a set of nuclei.

import pynucastro as pyna
import numpy as np
import matplotlib.pyplot as plt

We start by creating a Library object that reads all the ReacLib rates and link different nuclei of choice.

Then initialize NSENetwork by using the the rates created by the Library object.

library = pyna.ReacLibLibrary()

subch_full = library.linking_nuclei(["p", "he4",
                                     "c12", "n13", "n14",
                                     "o16", "f18", "ne20",
                                     "ne21", "na22", "na23",
                                     "mg24", "al27", "si28",
                                     "p31", "s32", "cl35",
                                     "ar36", "k39", "ca40",
                                     "sc43", "ti44", "v47",
                                     "cr48", "mn51",
                                     "fe52","co55","ni56"])

nse = pyna.NSENetwork(libraries=subch_full)

get_comp_nse(rho,T,ye) is a method of NSEState that returns composition at NSE as an object Composition by providing density, temperature, and a presribed electron fraction.

There is pre-set initial guess for the proton and neutron chemical potentials, however one should adjust the initial guess, accordingly if no solutions are found or taking a long time as the algorithm is searching for the correct initial guess. To pass in the initial guess, set initial_guess=guess. guess is a user-supplied list of the form [mu_p, mu_n], where mu_p and mu_n are the chemical potential for proton and neutron, respectively.

It has options of incorporating coulomb correction terms by let use_coulomb_corr=True, and return the solution of proton and neutron chemical potentials by return_sol=True.

comp, sol = nse.get_comp_nse(1e7, 6e9, 0.50, use_coulomb_corr=True, return_sol=True)
fig = comp.plot()
_images/4e4d060b7700acdcd6684cd4122e7b6b78e80f44ec37714168aefa7ee1fa332c.png

We can also explicitly print the composition

for c, v in comp.X.items():
    print(f"{str(c):6} : {v:20.12g}")
p      :     0.00934989821059
He4    :       0.433640616227
C12    :    7.72859959965e-06
N13    :    2.59190269463e-10
N14    :    1.36631324171e-09
O16    :    1.56975241119e-05
F18    :    2.41138161836e-11
Ne20   :    3.01874050001e-07
Ne21   :    3.95638586959e-09
Na22   :    9.68462282544e-10
Na23   :    7.29695210583e-08
Mg24   :    3.47019416232e-05
Al27   :     3.3525821184e-05
Si28   :      0.0104391634336
P31    :     0.00213500726862
S32    :     0.00916060183422
Cl35   :     0.00400094977488
Ar36   :     0.00487944875964
K39    :     0.00555275469044
Ca40   :     0.00477002847089
Sc43   :      0.0003726716421
Ti44   :    0.000229271130459
V47    :     0.00490466077421
Cr48   :      0.0011465823045
Mn51   :      0.0689585743911
Fe52   :     0.00576589690229
Co55   :       0.415686733967
Ni56   :      0.0189151049137
print(f"After solving, the chemical potential for proton and neutron are {sol[0]:10.7f} and {sol[1]:10.7f}")
After solving, the chemical potential for proton and neutron are -7.7386289 and -9.9990481

For \(\rho = 10^7~\mathrm{g~cm^{-3}}\) and electron fraction, \(Y_e = 0.5\), we can explore how the NSE state changes with temperature.

Here we used a guess for chemical potential of proton and neutron of -6.0 and -11.5, respectively.

rho = 1e7
ye = 0.5
temps = np.linspace(2.5, 12.0, 100)
X_s = []

guess = [-6.0, -11.5]
for i, temp in enumerate(temps):
    nse_comp, sol = nse.get_comp_nse(rho, temp*1.0e9, ye, init_guess=guess, 
                                     use_coulomb_corr=True, return_sol=True)
    guess = sol
    nse_X_s = [nse_comp.X[nuc] for nuc in nse_comp.X]
    X_s.append(nse_X_s)
X_s = np.array(X_s)
nuc_names = nse.get_nuclei()

low_limit = 1e-2
fig, ax = plt.subplots()

for k in range(len(nuc_names)):
    line, = ax.plot(temps, X_s[:,k])
    if (max(X_s[:,k]) > low_limit):
        line.set_label(str(nuc_names[k]))

ax.legend(loc = "upper right")
ax.set_xlabel(r'Temp $[\times 10^9 K]$')
ax.set_ylabel('Mass Fraction')
ax.set_yscale('log')
ax.set_ylim([low_limit,1.1])
ax.set_title(rf"$Y_e = {ye}$, $\rho = {rho:6.3g}$")

fig.set_size_inches((7, 6))
_images/3484a41f45baca758d608c5afad26e3579f42ddee241ca096c1c777493bdd652.png

For \(\rho = 10^7~\mathrm{g~cm^{-3}}\) and a relatively high temperature of \(T = 6 \times 10^9~\mathrm{K}\), we can look at how the NSE state changes with varying \(Y_e\).

Note: there will be a lowest \(Y_e\) that is attainable by the network, depending on what species are represented. As long as protons are in the network, we can reach a \(Y_e\) of \(1\).

ye_low = min(nuc.Z/nuc.A for nuc in nse.unique_nuclei)
rho = 1e7
ye_s = np.linspace(ye_low, 0.65, 100)
temp = 6.0e9
X_s_1 = []

guess = [-6.0, -11.5]
for i, ye in enumerate(ye_s):
    nse_comp_1, sol = nse.get_comp_nse(rho, temp, ye, init_guess=guess, 
                                       use_coulomb_corr=True, return_sol=True)
    guess = sol
    nse_X_s_1 = [nse_comp_1.X[nuc] for nuc in nse_comp_1.X]
    X_s_1.append(nse_X_s_1)
X_s_1 = np.array(X_s_1)
nuc_names = nse.get_nuclei()

fig, ax = plt.subplots()

for k in range(len(nuc_names)):
    line, = ax.plot(ye_s, X_s_1[:,k])
    if (max(X_s_1[:,k]) > 0.01):
        line.set_label(str(nuc_names[k]))

ax.legend(loc="best")
ax.set_xlabel('ye')
ax.set_ylabel('Mass Fraction')
ax.set_yscale('log')
ax.set_ylim([0.01,1])
ax.set_title(rf"$\rho = {rho:6.3g}$ and $T={temp:6.3g}$")

fig.set_size_inches((7, 6))
_images/4bdcdbbdc7677004530a6af30ab53c5abaa67aa0841ee59cd691dd9b2472085f.png