Chemical reactions

Hi again -still a very new user, gradually building up my model!

I’m wondering if there is any functionality within FESTIM to do chemical reactions or phase changes - specifically from hydride to mobile solute (as a function of a TSSd curve (which is fn. of temperature, but which I can make a fn. of x, for a given temperature profile).
I’ve looked at the radioactive decay, but not sure I can make that work as I need it to… plus I need my hydride to change into solute and vice versa (not just decay). Thanks in advance all! Happy Christmas, Helen

Hi @HelenS

When there’s a will there’s a way.

The development version of FESTIM (on the fenicsx branch) as a built-in functionality for chemical reactions A + B ↔ C + D.

This doesn’t exist in FESTIM1 (current version) but that doesn’t mean it cannot be done. If you don’t mind sharing the mathematical expression for the hydride formation it would greatly help!

From what I understand it should be something like:

H + Zr ↔ HZr

if c_i is the concentration of species i then we have

\frac{dc_H}{dt} = \frac{dc_{Zr}}{dt} = -\frac{dc_{HZr}}{dt} = -k c_H \ c_{Zr} + p c_{HZr}

where k and p are the forward and backward rates, respectively, and can depend on temperature.

If this is correct then we should be able to adapt the Trap class for this.

Hi Remi,
Just wanted to say thankyou so much for all your help with learning FESTIM. I’m on Christmas holidays now til early Jan. I need to revisit our older program to remind myself of the h in solution to h hydride equations. Because of the nature of this older program, we had to assign everything with a reaction rate (which was faster the further away from the TSSd), and at the TSSd concentration of hydrogen, the forward and backward rate were equal. With a program with more functionality, there may be a better way to do it but I need to give it some thought.
Anyway, Thankyou again, have a great Christmas break and speak soon, all best wishes, Helen

Hi - unfortunately this isn’t quite how I need to code it.
Let’s put cH as the concentration of H in solution, and c_hydride as the concentration of H that is hydride. then:

d(cH)/dt = -kF * (cH) + kB

kF is the precipitation rate, which we want to set to 1e16/TSSd.
kB is the hydride dissolution rate, which is equal to 1e16*exp(-e^-10/(RAMP (c_hydride + e^-13))
“RAMP” takes a value of zero when there is no hydride, or c_hydride when hydride is present.

So it’s not a standard equation where both rates also depend on the concentration of species present. Any ideas please?

Hi @HelenS thanks for clarifying

\frac{\partial c_H}{\partial t} = - \frac{\partial c_{hydride}}{\partial t} = -k_F \ c_H + k_B

with

k_F = \frac{10^{16}}{TSSd}

and

k_B = 10^{16} \ e^{-\frac{10^{-10}}{f_{RAMP}(c_{hydride})}}

Is there an analytical expression for TSSd? for RAMP (sigmoid?)?

Indeed this doesn’t behave as a classical trapping site.

The way to code this reaction would be to create a new class inheriting from festim.Trap and overriding the create_trapping_form method with the appropriate formulation. I’ll try to come up with an example

Actually could we write k_B simply as:

k_B = 10^{16} \ e^{-\frac{10^{-10}}{c_{hydride}}}

that sounds feasible - I will give that a go, with that simpler kB expression. Otherwise a sigmoid function for RAMP could work. TSSd is just function of temperature… for the moment, I have the same temperature everywhere so I can make TSSd a constant. Thanks so much as ever for your prompt replies

1 Like

Give me a few minutes and I should be able to provide an example

Here’s a working example. I’ll try to walk you through the methodology:

Step 1: Write a custom trap class

We write a custom class called Hydride that is inheriting from Trap and we override the __init__ method (construction) and the create_trapping_form method with the correct trapping term.

import festim as F
import fenics as f

class Hydride(F.Trap):
    def __init__(self, TSSd, materials):
        self.TSSd = TSSd
        super().__init__(k_0=0, E_k=0, p_0=0, E_p=0, materials=materials, density=0, id=None)

    def create_trapping_form(self, mobile, materials, T, dx, dt=None):
        """
        Args:
            mobile (festim.Mobile): the mobile concentration of the simulation
            materials (festim.Materials): the materials of the simulation
            T (festim.Temperature): the temperature of the simulation
            dx (fenics.Measure): the dx measure of the sim
            dt (festim.Stepsize, optional): If None assuming steady state.
                Defaults to None.
        """
        solution = self.solution
        prev_solution = self.previous_solution
        test_function = self.test_function

        if not all(isinstance(mat, F.Material) for mat in self.materials):
            self.make_materials(materials)

        expressions_trap = []
        F_trapping = 0  # initialise the form

        if dt is not None:
            # d(c_t)/dt in trapping equation
            F_trapping += ((solution - prev_solution) / dt.value) * test_function * dx
        else:
            # if the sim is steady state and
            # if a trap is not defined in one subdomain
            # add c_t = 0 to the form in this subdomain
            for mat in materials:
                if mat not in self.materials:
                    F_trapping += solution * test_function * dx(mat.id)

        for i, mat in enumerate(self.materials):

            if isinstance(mobile, F.Theta) and mat.solubility_law == "henry":
                raise NotImplementedError(
                    "Henry law of solubility is not implemented with traps"
                )

            c_0, c_0_n = mobile.get_concentration_for_a_given_material(mat, T)

            # ------- WE OVERRIDE THIS PART ---- >>>>>

            
            F_trapping += -self.coupling_term(c_0) * test_function * dx(mat.id)

            # ----------------------------------- <<<<<


        self.F_trapping = F_trapping
        self.F += self.F_trapping
        self.sub_expressions += expressions_trap
        
    def coupling_term(self, c_mobile):
        # dc_hydride/dt = k_F * c_mobile - k_B
        k_F = 1e16 / self.TSSd
        k_B = 1e16 * f.exp(-1e-10/(self.solution + f.DOLFIN_EPS))  # added epsilon to avoid division by zero
        return k_F * c_mobile - k_B

Step 2 monkey patching festim.Mobile.create_diffusion_form

A current limitation of FESTIM 1 is that users don’t explicitly define the mobile concentration instance + only the “classical” trapping form is considered.

Therefore we need to “mokey patch” the Mobile class to account for this new expression.

# Monkey patch the F.Mobile class (the create_diffusion_form method)

def create_diffusion_form(
    self, materials, mesh, T, dt=None, traps=None, soret=False
):
    """Creates the variational formulation for the diffusive part.

    Args:
        materials (festim.Materials): the materials
        mesh (festim.Mesh): the mesh
        T (festim.Temperature): the temperature
        dt (festim.Stepsize, optional): the stepsize. Defaults to None.
        traps (festim.Traps, optional): the traps. Defaults to None.
        chemical_pot (bool, optional): if True, conservation of chemical
            potential is assumed. Defaults to False.
        soret (bool, optional): If True, Soret effect is assumed. Defaults
            to False.
    """
    from fenics import dot, grad, exp, SpatialCoordinate
    from festim import k_B
    F = 0
    for material in materials:
        D_0 = material.D_0
        E_D = material.E_D
        c_0, c_0_n = self.get_concentration_for_a_given_material(material, T)

        subdomains = material.id  # list of subdomains with this material
        if not isinstance(subdomains, list):
            subdomains = [subdomains]  # make sure subdomains is a list

        # add to the formulation F for every subdomain
        for subdomain in subdomains:
            dx = mesh.dx(subdomain)
            # transient form
            if dt is not None:
                F += ((c_0 - c_0_n) / dt.value) * self.test_function * dx
            D = D_0 * exp(-E_D / k_B / T.T)
            if mesh.type == "cartesian":
                F += dot(D * grad(c_0), grad(self.test_function)) * dx
                if soret:
                    Q = material.Q
                    if callable(Q):
                        Q = Q(T.T)
                    F += (
                        dot(
                            D * Q * c_0 / (k_B * T.T**2) * grad(T.T),
                            grad(self.test_function),
                        )
                        * dx
                    )

            # see https://fenicsproject.discourse.group/t/method-of-manufactured-solution-cylindrical/7963
            elif mesh.type == "cylindrical":
                r = SpatialCoordinate(mesh.mesh)[0]
                F += r * dot(D * grad(c_0), grad(self.test_function / r)) * dx

                if soret:
                    Q = material.Q
                    if callable(Q):
                        Q = Q(T.T)
                    F += (
                        r
                        * dot(
                            D * Q * c_0 / (k_B * T.T**2) * grad(T.T),
                            grad(self.test_function / r),
                        )
                        * dx
                    )

            elif mesh.type == "spherical":
                r = SpatialCoordinate(mesh.mesh)[0]
                F += (
                    D
                    * r
                    * r
                    * dot(grad(c_0), grad(self.test_function / r / r))
                    * dx
                )

                if soret:
                    Q = material.Q
                    if callable(Q):
                        Q = Q(T.T)
                    F += (
                        D
                        * r
                        * r
                        * dot(
                            Q * c_0 / (k_B * T.T**2) * grad(T.T),
                            grad(self.test_function / r / r),
                        )
                        * dx
                    )

    # add the trapping terms
    F_trapping = 0
    if traps is not None:
        for trap in traps:
            for i, mat in enumerate(trap.materials):
                if isinstance(trap, Hydride):
                    c_m, _ = self.get_concentration_for_a_given_material(mat, T)
                    F_trapping += -trap.coupling_term(c_m) * self.test_function * dx(mat.id)
                else:
                    if type(trap.k_0) is list:
                        k_0 = trap.k_0[i]
                        E_k = trap.E_k[i]
                        p_0 = trap.p_0[i]
                        E_p = trap.E_p[i]
                        density = trap.density[i]
                    else:
                        k_0 = trap.k_0
                        E_k = trap.E_k
                        p_0 = trap.p_0
                        E_p = trap.E_p
                        density = trap.density[0]
                    c_m, _ = self.get_concentration_for_a_given_material(mat, T)
                    F_trapping += (
                        -k_0
                        * exp(-E_k / k_B / T.T)
                        * c_m
                        * (density - trap.solution)
                        * self.test_function
                        * dx(mat.id)
                    )
                    F_trapping += (
                        p_0
                        * exp(-E_p / k_B / T.T)
                        * trap.solution
                        * self.test_function
                        * dx(mat.id)
                    )
    F += -F_trapping

    self.F_diffusion = F
    self.F += F


F.Mobile.create_diffusion_form = create_diffusion_form

Step 3 Integrate in a FESTIM model and plot results

Here, we use the newly created Hydride class in the traps of a festim model.
This is a simple 1D model with insulated boundaries and an initial concentration of mobile particles (equivalent to a 0D model).

import numpy as np

my_model = F.Simulation()

my_model.mesh = F.MeshFromVertices(np.linspace(0, 1e-3, 500))

my_model.materials = F.Material(id=1, D_0=1, E_D=0)

my_model.traps = [Hydride(TSSd=1e15, materials=my_model.materials)]

my_model.initial_conditions = [
    F.InitialCondition(field="solute", value=1e16)
]

my_model.T = 600

my_model.settings = F.Settings(
    absolute_tolerance=1e6,
    relative_tolerance=1e-10,
    final_time=1,
)

my_model.dt = F.Stepsize(initial_value=0.01)

c_mobile_export = F.AverageVolume(field="solute", volume=1)
c_hydride_export = F.AverageVolume(field=1, volume=1)
my_model.exports = [F.DerivedQuantities([c_mobile_export, c_hydride_export])]


# my_model.log_level = 20
my_model.initialise()
my_model.run()


# plot the results

import matplotlib.pyplot as plt

plt.plot(c_mobile_export.t, c_mobile_export.data, label="c_mobile")
plt.plot(c_hydride_export.t, c_hydride_export.data, label="c_hydride")
plt.legend()
plt.yscale("log")

plt.xlabel("Time (s)")
plt.ylabel("Concentration (m^-3)")

plt.show()