Concentration#
Initial conditions are an important part of transient simulations for both hydrogen transport and heat-transfer problems. FESTIM defaults to zero values for initial conditions, but can be set using FESTIM’s InitialConditionBase.
This tutorial discusses how to set initial conditions for particle concentrations.
Objectives:
Setting the initial concentration for a single species
Defining initial concentration conditions for multiple species
Setting the initial concentration for a single species#
The InitialConcentration class can be used for defining initial conditions for the concentrations, which must defined on a volume subdomain in a FESTIM simulation:
import festim as F
material = F.Material(D_0=1, E_D=0)
vol = F.VolumeSubdomain(id=1, material=material)
H = F.Species("H")
IC = F.InitialConcentration(value=2, species=H, volume=vol)
Initial conditions can also be a function of space x and temperature T, such as:
my_function = lambda x,T: 2*x[0] + 3*x[1] + 5*T
IC = F.InitialConcentration(value=my_function, species=H, volume=vol)
Multi-species initial conditions#
The same class InitialConcentration can be used for multiple species, but a separate initial condition is required for each species.
Consider the following 2D example, where we have three species (hydrogen, deuterium, and tritium) with initial and boundary conditions for species’ concentrations:
First, we can create a 2D unit square mesh and mark the subdomains:
import festim as F
from dolfinx.mesh import create_unit_square
from mpi4py import MPI
import numpy as np
nx, ny = 50, 50
mesh = create_unit_square(MPI.COMM_WORLD, nx, ny)
model = F.HydrogenTransportProblem()
model.mesh = F.Mesh(mesh)
material = F.Material(D_0=1e-2, E_D=0)
top_surface = F.SurfaceSubdomain(id=1, locator = lambda x: np.isclose(x[1], 1.0))
bottom_surface = F.SurfaceSubdomain(id=2, locator = lambda x: np.isclose(x[1], 0.0))
right_surface = F.SurfaceSubdomain(id=3, locator = lambda x: np.isclose(x[0], 1.0))
left_surface = F.SurfaceSubdomain(id=4, locator = lambda x: np.isclose(x[0], 0.0))
vol = F.VolumeSubdomain(id=1, material=material)
model.subdomains = [top_surface, bottom_surface, left_surface, right_surface, vol]
Now, let’s define our species and initial conditions. We must create an InitialConcentration for each species, then we can pass the initial conditions into the initial_conditions attribute:
H = F.Species("H")
D = F.Species("D")
T = F.Species("T")
IC_H = F.InitialConcentration(value=4, species=H, volume=vol)
IC_D = F.InitialConcentration(value=5, species=D, volume=vol)
IC_T = F.InitialConcentration(value=6, species=T, volume=vol)
model.species = [H, D, T]
model.initial_conditions = [IC_H, IC_D, IC_T]
Note
To pass the initial conditions into FESTIM, you must use a list!
We also define the zero concentration boundary conditions for each species. Let’s run the simulation for 5 seconds with a stepsize of 1 second, and visualize the final concentration profiles (hydrogen on the left, deuterium in the middle, and tritium on the right):
model.boundary_conditions = [
F.FixedConcentrationBC(subdomain=left_surface, value=0, species=H),
F.FixedConcentrationBC(subdomain=right_surface, value=0, species=H),
F.FixedConcentrationBC(subdomain=top_surface, value=0, species=H),
F.FixedConcentrationBC(subdomain=left_surface, value=0, species=D),
F.FixedConcentrationBC(subdomain=right_surface, value=0, species=D),
F.FixedConcentrationBC(subdomain=top_surface, value=0, species=D),
F.FixedConcentrationBC(subdomain=left_surface, value=0, species=T),
F.FixedConcentrationBC(subdomain=right_surface, value=0, species=T),
F.FixedConcentrationBC(subdomain=top_surface, value=0, species=T),
]
model.temperature = 400
model.settings = F.Settings(atol=1e-10, rtol=1e-10, final_time=50, stepsize=5)
model.initialise()
model.run()
2026-09-05 11:27:34.130 ( 0.812s) [ 709126556440]vtkXOpenGLRenderWindow.:1416 WARN| bad X server connection. DISPLAY=
Using a dolfinx.fem.Function as initial condition#
It is possible to use a dolfinx.fem.Function() as initial condition.
This is particularly useful when using the result of a previous simulation for a restart.
from mpi4py import MPI
import dolfinx
mesh = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, 10, 10)
V = dolfinx.fem.functionspace(mesh, ("CG", 1))
u_ini = dolfinx.fem.Function(V)
u_ini.interpolate(lambda x: 2e4 * (x[0] * (1 - x[0]) * x[1] * (1 - x[1]))**4)
import festim as F
my_model = F.HydrogenTransportProblem()
my_model.mesh = F.Mesh(mesh)
material = F.Material(D_0=0.1, E_D=0)
vol = F.VolumeSubdomain(id=1, material=material)
my_model.subdomains = [vol]
H = F.Species("H")
my_model.species = [H]
my_model.initial_conditions = [F.InitialConcentration(value=u_ini, species=H, volume=vol)]
my_model.temperature = 300
my_model.settings = F.Settings(atol=1e-10, rtol=1e-10, stepsize=0.05, final_time=0.3)
my_model.initialise()
my_model.run()
Initial condition from a checkpoint file#
A simulation can be restarted from a checkpoint written by an earlier one. Unlike the visualisation formats, a checkpoint stores the field in its own function space, so it comes back exactly as it was written – see Checkpointing.
Here we run a short simulation and checkpoint the hydrogen field with
format="checkpoint":
first_model.exports = [
F.SpeciesExport("checkpoint_file.bp", field=H, subdomain=vol, format="checkpoint")
]
first_model.initialise()
first_model.run()
festim.read_function_from_file() reads it back, given the name of the species and the
timestamp to read:
u_in = F.read_function_from_file(
filename="checkpoint_file.bp", name="H", timestamp=10.0
)
print(u_in.x.array[:])
[0.03005561 0. 0. 0.02977179 0.06506472 0.
0.06470054 0.02960064 0.1104765 0. 0.11010279 0.06442949
0.02947718 0.17153586 0. 0.17120929 0.10978874 0.06421289
0.0293761 0.25296247 0. 0.25274836 0.17090498 0.10951651
0.06402678 0.02928454 0.35826639 0. 0.35823039 0.25250925
0.17061864 0.10927084 0.0638548 0.02919326 0.48884177 0.
0.48902436 0.35810715 0.25225337 0.17034557 0.10903844 0.06368324
0.02909279 0.64311408 0. 0.64349284 0.48904285 0.35792221
0.25198918 0.17007986 0.10880639 0.06349798 0.02896965 0.81613105
0. 0.8165577 0.64362717 0.48895119 0.35770136 0.25172209
0.16981423 0.1085613 0.06328158 0.02879758 1. 0.
1. 0.81670677 0.64361798 0.48879679 0.35746461 0.25145464
0.16954093 0.10828856 0.06300844 0.02851048 1. 0.81673189
0.64353313 0.48861462 0.3572271 0.25118911 0.16925292 0.10797101
0.06263777 1. 0.81669986 0.64341609 0.48843155 0.35700398
0.2509304 0.16894419 0.10758865 1. 0.81664501 0.64329842
0.48827471 0.3568156 0.25068708 0.16860952 1. 0.81658998
0.64321205 0.48818014 0.35668965 0.25046902 1. 0.8165578
0.64320175 0.48819822 0.35665636 1. 0.81658388 0.64333797
0.48838951 1. 0.81673631 0.64372681 1. 0.81716812
1. ]
Note
Pass backend="h5py" if the checkpoint was written to a plain .h5 file rather than the ADIOS2
default. Only checkpoints can be read back this way: the visualisation formats store values
interpolated onto the mesh nodes rather than the degrees of freedom.
u_in is an ordinary dolfinx.fem.Function, so it can be used as an initial condition for a
species exactly as explained in Using a dolfinx.fem.Function as initial condition:
my_model.initial_conditions = [
F.InitialConcentration(value=u_in, species=H, volume=vol)
]
Warning
The function read back lives on the mesh stored in the checkpoint, which is a different object
from any mesh you build in the new script – even one with identical geometry. Build the restarted
model on that mesh, reachable as u_in.function_space.mesh, rather than on a freshly created one:
interpolating between two distinct meshes is not supported and raises.