FiPy 1D Diffusion Solver for Stellar Species Abundances

diffusion.py

A Python script using the FiPy finite-volume PDE solver to model one-dimensional diffusion of chemical species on a mass grid. It defines a 1D mesh, initializes abundances for H1 and He4, computes a diffusion coefficient, and solves a diffusion equation with operator splitting, while optionally visualizing the evolving abundances via matplotlib animation. Includes a dummy diffusion function and notes about mass conservation and integration with a nuclear network.

from fipy import Grid1D, CellVariable, FaceVariable, TransientTerm, DiffusionTerm
import numpy as np
 
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
 
from scipy.integrate import trapezoid
 
# This is a stand in for your more robust diffision function
def Dumnmy_get_diffusion(nx):
    r = np.linspace(0.1, 1.0, nx)
    D = (2*r-1)**2 + np.exp(-100*(r-0.5)**2)
    plt.plot(r, D)
    plt.show()
 
    return D
 
# Setting up grid / discritization. Most of this will be informed from inputs you recive from other modules
m_min, m_max = 0, 1.0  
nx = 100
 
# If you choose to use FiPy then you will need to work in its framework, this means you need to take the grid of 
# masses george has generated and turn them into a Grid1D
 
# There are alternate Grid1D constructors which let you take in a preexisting grid, try to look these up in the
# FiPy documentation if you decide to go this route
mesh = Grid1D(nx=nx, dx=(m_max - m_min) / nx)
 
# These are teh initial conditions, I have done some very approximate stuff here to make an interesting
# demo case. Generally you will recive these from either george or sasha
species = ['H1', 'He4']
Y = {s: CellVariable(mesh=mesh, value=0.1, hasOld=True) for s in species}
 
initial_H1 = np.where(mesh.cellCenters[0] < 0.5, 0.7, 0.2)
Y['H1'].setValue(initial_H1)
 
initial_He4 = np.where(mesh.cellCenters[0] < 0.5, 0.3, 0.8)
Y['He4'].setValue(initial_He4)
 
# I am using this python module FiPy, its a Finite Volume PDE solver
# Finite volumes are good for this class of problem as they deal with conservation laws inherently
# and tend to be more numerically stable than finite difference. 
 
# These are variables which are different for each cell. Note how I am just using numpy arrays as the
# values. You already calculate D so you might use that as the value
r_cell = CellVariable(mesh=mesh, value=np.linspace(0.1, 1.0, nx))
rho_cell = CellVariable(mesh=mesh, value=np.ones(nx))
D_cell = CellVariable(mesh=mesh, value=Dumnmy_get_diffusion(nx))
 
A_face = (4 * np.pi * r_cell.faceValue**2 * rho_cell.faceValue)**2 * D_cell.faceValue
 
eqs = {s: TransientTerm() == DiffusionTerm(coeff=A_face) for s in species}
 
 
 
# This is just some plotting code to make visualization
 
fig, ax = plt.subplots(figsize=(8, 5))
ax.set_xlim(0, m_max)
ax.set_ylim(0, 1.1)
ax.set_xlabel('Mass Coordinate (m)')
ax.set_ylabel('Molar Abundance (Yi)')
ax.set_title('Species Abundance Evolution')
 
lines = {}
for name in species:
    line, = ax.plot(mesh.cellCenters[0], Y[name].value, label=name, lw=2)
    lines[name] = line
 
ax.legend()
 
# Diffusion timescales are very different than nuclear timescales, therefore it is often
# useful to solve these systems seperately (operator-splitting) using a much finer timestep
dt = 1e-5
 
# In this case I am letting matplotlib handle the timestepping loop, this is useful for animation
# however, not great for actual code. You could replace this by just the inner for loop
 
last_amount = {s: c.value for s, c in Y.items()}
def update(frame):
    for s in species: # Solve indeendencly for each species. This removes cross-diffusion, thought thats okay in this case
 
        # Here you might do something like (in an operator-splitting approach)
        # Note how we are just getting the new_abundances from whatever the nuclear netwoek says
        # and we use those as the initial conditions. 
 
        # new_abundances = nuclear(...)
        # Y[s].setValue(new_abundances)
 
        # Some complexity to think about here, though using
        # a coroutine may make this a bit simpler to implement
 
        Y[s].updateOld()
        eqs[s].solve(var=Y[s], dt=dt)
 
        lines[s].set_ydata(Y[s].value)
        newS = trapezoid(Y[s].value)
 
        ds = newS - last_amount[s]
        last_amount[s] = newS
 
        print(f"For Species {s:10} mass leakage is: {ds}")
 
    return list(lines.values())
 
animation = FuncAnimation(fig, update, frames=2000, interval=50, blit=True)
 
plt.show()
 
# Some things to try
#   1. Play around with getting your Diffision Function in there
#   2. See how changing the timestep size affects results
#   3. Try to get sashs outputs into here (they will come in as R_nuc) and then to return new abundances to george
#   4. Check mass conservation, solvers have a tendency to let mass "leak" out, when this happens re-normalization stages are often required