Solver does not converge for "Deuterium retention in self-damaged tungsten" with sharper drop in damage distribution

I’m trying to model deuterium retention in tungsten damaged by 20 MeV tungsten ions. Since the D retention is saturated when the damage is above 0.1 dpa, I want to use a following damage depth distribution shown in orange (which is modified from the SRIM calculated depth profile shown in blue).

I tried to modify the damage depth distribution f(x) = \frac{1}{1 + \exp(\frac{x - x_{0}}{\Delta x}) } in Deuterium retention in self damaged tungsten such that x_{0} and \Delta x are now 1.8e-6 and 9e-8, respectively, leading to a sharper drop in damage depth distribution compared to the example. The solver then has trouble converging (i.e., the error message reads “ValueError: stepsize reached minimal value”), even when reducing the minimum timestep from 1e-1 to 1e-5, the relative tolerance from 1e-10 to 1e-5, and mesh near the drop off from ~80 nm to ~0.3 nm. I also tried to replace the damage depth distribution to use a hyperbolic tangent (i.e., \frac{1}{2}(tanh(- \frac{x - x_{0}}{\Delta x}) + 1) with x_{0} and \Delta x set to 1.8e-7 and 1.7e-7, respectively) instead of exponential, but I still can’t get the solver to converge. Any suggestions for how to resolve the convergence issue?

Note that I am using Festim version 1. Below is a copy of my code.

import festim as F
import numpy as np
import sympy as sp
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.use('tkagg')
import os, shutil

file_name = "tds/derived_quantities_example_TDSfromdamagedW.csv"
data_folder_path = '/home/pisces/examples/damagedesorb/tds/'
if os.path.isdir(data_folder_path):
    shutil.rmtree(data_folder_path)
os.mkdir(data_folder_path)

# # ### Parameters ###
D_0 = 1.6e-7  # m^2 s^-1
E_D = 0.28  # eV
w_atom_density = 6.3222e28
sample_thickness = 0.8e-3  # m
sample_area = 12e-03 * 15e-03

detrapping_energies = [1.15, 1.35, 1.65, 1.85, 2.05]
num_traps = len(detrapping_energies) + 1 # including intrinsic trap
dpa_n_i = {
    0.001: [1e24, 2.5e24, 1e24, 1e24, 2e23],
    0.1: [4.8e25, 3.8e25, 2.6e25, 3.6e25, 1.1e25],
}

# Table 2 from Dark et al 10.1088/1741-4326/ad56a0
T_imp = 370  # K
T_rest = 295  # K
R_p = 0.7e-9  # m
sigma = 0.5e-9  # m
t_imp = 72 * 3600  # s
implantation_time = t_imp
t_rest = 60#12 * 3600  # s
resting_time = t_rest
Beta = 3 / 60  # K s^-1
fluence = 1.5e25
flux = fluence / t_imp
start_tds = t_imp + t_rest  # s
min_temp, max_temp = 300, 1000
export_times = [implantation_time]

##vertices = sample_thickness/100*np.genfromtxt("mesh.dat",delimiter=",")

def festim_sim(densities):

    model = F.Simulation()
    vertices = np.concatenate(
        [
            np.linspace(0, 3e-9, num=100),
            np.linspace(3e-9, 3e-6, num = 10000),
            np.linspace(3e-6, 8e-6, num = 100),
            np.linspace(8e-6, 8e-5, num=100),
            np.linspace(8e-5, sample_thickness, num=100),
        ]
    )
    model.mesh = F.MeshFromVertices(vertices)
    
    # ### Material ###
    damaged_tungsten = F.Material(1, D_0, E_D)
    model.materials = damaged_tungsten
    
    # ### Source ###
    # Deuterium Beam Profile (S = flux * f(x))
    distribution = (
        1 / (sigma * (2 * np.pi) ** 0.5) * sp.exp(-0.5 * ((F.x - R_p) / sigma) ** 2)
    )
    ion_flux = sp.Piecewise((flux * distribution, F.t < t_imp), (0, True))
    source_term = F.Source(value=ion_flux, volume=1, field=0)
    model.sources = [source_term]
    
    # ### Boundary Conditions ###
    model.boundary_conditions = [
        F.DirichletBC(surfaces=[1, 2], value=0, field="solute")
    ]
    
    # ### Temperature ###

    model.T = F.Temperature(
        value=sp.Piecewise(
            (T_imp, F.t < t_imp),
            (T_rest, F.t < start_tds),
            (min_temp + Beta * (F.t - start_tds), True),
        )
    )
    
    # ### Trap Settings ###
    k_0 = D_0 / (1.1e-10**2 * 6 * w_atom_density)
    trap_1 = F.Trap(
        E_k=damaged_tungsten.E_D,
        k_0=k_0,
        E_p=1.04,
        p_0=1e13,
        density=2.4e22,
        materials=damaged_tungsten,
    )
    neutron_induced_traps = []
    damage_dist = 1 / (1 + sp.exp((F.x - 1.8e-6) / 9e-8)) # 0.5 * (sp.tanh(- (F.x - 1.8e-06) / 1.7e-07) + 1)#
    for E_p, density in zip(detrapping_energies, densities):
        neutron_induced_traps.append(
            F.Trap(
                k_0=k_0,
                E_k=damaged_tungsten.E_D,
                p_0=1e13,
                E_p=E_p,
                density=density * damage_dist,
                materials=damaged_tungsten,
            )
        )
    model.traps = [trap_1] + neutron_induced_traps

    model.dt = F.Stepsize(
        initial_value=1,
        stepsize_change_ratio=1.1,
        dt_min=1e-5,
        t_stop=resting_time - 20,
        stepsize_stop_max=50,
        milestones=export_times,
    )
    model.settings = F.Settings(
        absolute_tolerance=1e10,
        relative_tolerance=1e-5,
        final_time=start_tds + (max_temp - min_temp) / Beta,  # time to reach max temp
        traps_element_type="DG"
    )
    model.log_level = 20
    
    list_of_derived_quantities = [
        F.TotalVolume("solute", volume=1), 
        F.TotalVolume("retention", volume=1),
        F.HydrogenFlux(surface=1),
        F.HydrogenFlux(surface=2),
    ]
    list_of_derived_quantities += [
        F.TotalVolume(f"{i}", volume=1) for i in range(1, num_traps + 1)
    ]
    derived_quantities = F.DerivedQuantities(list_of_derived_quantities,filename=file_name)
    model.exports = [derived_quantities,
        F.TXTExport(
            field="retention",
            times=export_times,
            folder="tds",
            label="retention",
        ),
        F.TXTExport(
            field="solute",
            times=export_times,
            folder="tds",
            label="solute",
        ),
        F.TXTExport(
            field="1",
            times=export_times,
            folder="tds",
            label="trap1",
        ),
        F.TXTExport(
            field="2",
            times=export_times,
            folder="tds",
            label="trap2",
        ),
        F.TXTExport(
            field="3",
            times=export_times,
            folder="tds",
            label="trap3",
        ),
        F.TXTExport(
            field="4",
            times=export_times,
            folder="tds",
            label="trap4",
        ),
        F.TXTExport(
            field="5",
            times=export_times,
            folder="tds",
            label="trap5",
        ),
        F.TXTExport(
            field="6",
            times=export_times,
            folder="tds",
            label="trap6",
        ),
    ]
    model.initialise()
    model.run()

    t = np.array(derived_quantities.t)
    retention = np.array(derived_quantities.filter(fields="retention").data)
    flux_left = np.array(derived_quantities.filter(fields="solute", surfaces=1).data)
    flux_right = np.array(derived_quantities.filter(fields="solute", surfaces=2).data)
    flux_total = -flux_left - flux_right
    temp = min_temp + Beta * (t - start_tds)
    idx = np.where(t > start_tds)

    plt.figure(1)
    plt.ylabel(r"Total Retention (m$^{-1}$)")
    plt.xlabel(r"Time (s)")
    plt.plot(t,retention, label = f"{dpa}")
    plt.legend()
    
    plt.figure(2)   
    plt.plot(temp[idx], flux_total[idx], linestyle="dashed", color="tab:grey", linewidth=2)

    if dpa == 0.1: 
        plt.figure(3)
        plt.title("Damage = 0.1 dpa")
        plt.ylabel(r"Desorption flux (m$^{-2}$ s$^{-1}$)")
        plt.xlabel(r"Temperature (K)")
        plt.plot(temp[idx], flux_total[idx], linewidth=3, label="FESTIM")

        colors = [(0.9 * (i % 2), 0.2 * (i % 4), 0.4 * (i % 3)) for i in range(num_traps)] 
        trap_data = [derived_quantities.filter(fields=f"{i}").data for i in range(1, num_traps + 1)]
        contributions = [
            -np.diff(np.array(trap)[idx]) / np.diff(t[idx]) for trap in trap_data
        ]
        for i, cont in enumerate(contributions):
            if i == 0:
                label = "Trap 1"
            else:
                label = f"Trap D{i}"
            plt.plot(temp[idx][1:], cont, linestyle="--", color=colors[i], label=label)
            plt.fill_between(temp[idx][1:], 0, cont, facecolor="grey", alpha=0.1)
        plt.legend()

    return derived_quantities

dpa_to_quantities = {}
for dpa, densities in dpa_n_i.items():
    dpa_to_quantities[dpa] = festim_sim(densities)

for i in [3, 2]:
    plt.figure(i)
    plt.ylabel(r"Desorption flux (m$^{-2}$ s$^{-1}$)")
    plt.xlabel(r"Temperature (K)")
    plt.ylim(bottom=0, top=1e17)
    ax = plt.gca()
    ax.spines["top"].set_visible(False)
    ax.spines["right"].set_visible(False)
    
from matplotlib import cm, colors
norm = colors.LogNorm(vmin=min(list(dpa_n_i.keys())[1:]), vmax=max(dpa_n_i.keys())) #using [1:] indexing to ignore 0
colorbar = cm.viridis
sm = plt.cm.ScalarMappable(cmap=colorbar, norm=norm)
# Plotting color bar
from mpl_toolkits.axes_grid1 import make_axes_locatable
plt.figure(2)
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.1)
plt.colorbar(sm, cax=cax, label="Damage (dpa)")


plt.figure(4)
x = np.genfromtxt("tds/solute_" + str(export_times[0]) + ".0s.txt")[:,0]
solute_x = np.genfromtxt("tds/solute_" + str(export_times[0]) + ".0s.txt")[:,1]
plt.semilogx(x, solute_x, label = "solute")
for i in range(num_traps):
    trap_x = np.genfromtxt(f"tds/trap{i+1}_" + str(export_times[0]) + ".0s.txt")[:,1]
    plt.semilogx(x, trap_x, label=f"trap {i+1}")
plt.legend()
plt.ylabel(r"Concentration (m$^{-3}$)")
plt.xlabel(r"Depth (m)")

plt.show()

Example_TDSfromdamagedW.zip (3.0 KB)

Hi @m2patino !

I haven’t got time to run the code right now (by the way I would recommend switching to FESTIM2!) .

Have you tried exporting the concentration fields to see what happens at the time of divergence?

The following are the concentration depth profiles at 236 s (the code doesn’t converge at 237 s). This is still during the implantation phase and the D diffusion front has not yet reached the drop off in damage depth distribution.

have you tried turning on the log level INFO to inspect the Newton iterations? chances are it’s a tolerance tweaking situation