UKAEA FESTIM Workshop 2026
Kaelyn Dunnell & Tez Orr
June 29, 2026
DFT
Y. Ferro et al 2023 Nucl. Fusion 63 036017
Length scale
Time scale
MD
Length scale
Time scale
DFT
potentials
Component scale modelling
Length scale
Time scale
MD
DFT
D, S, other coeffs.
Length scale
Time scale
MD
DFT
Component scale modelling
Fuel cycle modelling
Residency times, fluxes, ...
Length scale
Time scale
MD
DFT
Component scale modelling
Fuel cycle modelling
Abstraction
\( \varphi\): diffusion flux
\( D \): diffusion coefficient
\( c \): mobile hydrogen concentration
\( S\): source term
Fick's 1st law of diffusion
Fick's 2nd law of diffusion
H
Potential energy
Distance
Diffusion barrier
Trap binding energy
Trapping energy
Common assumption:
\( E_k = E_D \)
0D
Total concentration of traps
Ziegler et al. 2010. Nuclear Instruments and Methods in Physics Research Section B: Beam Interactions with Materials and Atoms, 268 (11): 1818–23. https://doi.org/10.1016/j.nimb.2010.02.091.
Implantation range
Implantation range & width and reflection coefficient can be computed with SRIM, SDTRIM...
Mutzke et al, SDTrimSP Version 6.00 2019
\(\Gamma_\mathrm{incident} \): incident flux (particle/m2/s)
\( f(x) \): Gaussian distribution (/m)
\(r \): reflection coefficient
or
H2 molecules
Metal lattice
Dissociation coefficient (H/m2/s/Pa)
Partial pressure of H (Pa)
Adsorbed H
Metal lattice
Metal lattice
Recombination coefficient (m4/s)
Concentration (H/m3)
Metal lattice
Waelbroeck model
Metal lattice
At equilibrium:
Sievert's law of solubility
Non-metallic liquid
At equilibrium:
Henry's law of solubility
Material 1
Material 2
Case 1:
Metal-Metal
Sievert's law
Flux continuity
Conservation of chemical potential
Material 1
Material 2
Case 2:
Non metal-non metal
Henry's law
Flux continuity
Conservation of chemical potential
Material 1
Material 2
Case 3:
Metal-Non metal
Sievert's law
Henry's law
McNabb & Foster model
Challenges
Component modelling
Experimental analysis
Finite Difference Method (FDM)
Finite Element Method (FEM)
Finite Volume Method (FVM)
Let's not bother
See more FESTIM stats:
festim-dev.github.io/pose
See all FESTIM papers:
Docs users
Fusion machine developers
Research organisations
Before
Now
~11x faster
Before
Now
Before
Now
velocity, temperature, turbulent viscosity...
tritium production
Heat source
Temperature
Neutronics
Thermo-hydraulics
Tritium transport
ARC blanket geometry
T source
Heat source
Velocity
Temperature
Turbulent viscosity
Tritium concentration
"Even though it's quite nice to be the FESTIM v2 expert on site (!!!), I would absolutely recommend the programme to my colleagues."
First FESTIM fellow - Tez Orr (UKAEA)
Hands-on-learning - Tez worked directly with the dev team on UKAEA cases and fixed a bug in v2.0
Immediate impact - Became UKAEA's resident FESTIM expert, contributing to both the LIBRTI and STEP programmes
Read a mesh from GMSH
mesh_data = gmshio.read_from_msh(
"gmsh/mesh3D.msh", MPI.COMM_WORLD, 0, gdim=3
)
mesh = mesh_data.mesh
facet_tags = mesh_data.facet_tags
cell_tags = mesh_data.cell_tagsCan also import meshes from other sources thanks to io4dolfinx
Run heat conduction simulation
heat_transfer_problem = F.HeatTransferProblem()
...
heat_flux_top = F.HeatFluxBC(subdomain=top_surface, value=10e6)
convective_flux_coolant = F.HeatFluxBC(
subdomain=cooling_surface, value=lambda T: h_convective * (T_coolant - T)
)
...
heat_transfer_problem.run()Convective cooling
Pass the temperature field to the H transport problem
my_model = F.HydrogenTransportProblemDiscontinuous()
my_model.temperature = heat_transfer_problem.u
my_model.method_interface = "penalty"
my_model.subdomains = all_subdomains
H = F.Species("H", subdomains=my_model.volume_subdomains)
my_model.species = [H]
my_model.mesh = mesh
my_model.surface_to_volume = {
top_surface: tungsten_volume,
cooling_surface: cucrzr_volume,
poloidal_gap_w: tungsten_volume,
poloidal_gap_cu: copper_volume,
poloidal_gap_cucrzr: cucrzr_volume,
toroidal_gap: tungsten_volume,
bottom: tungsten_volume,
}
penalty_term = 1e20
my_model.interfaces = [
F.Interface(
id=16, subdomains=(tungsten_volume, copper_volume), penalty_term=penalty_term
),
F.Interface(
id=17, subdomains=(copper_volume, cucrzr_volume), penalty_term=penalty_term
),
]
import ufl
# Plasma implantation flux BC
phi = 1.61e22
R_p = 9.52e-10
implantation_flux_top = F.FixedConcentrationBC(
subdomain=top_surface,
value=lambda T: phi * R_p / (tungsten.D_0 * ufl.exp(-tungsten.E_D / F.k_B / T)),
species=H,
)
# Instantaneous molecular recombination flux BCs at all other surfaces (fixed concentration of 0)
recombination_fluxes = [
F.FixedConcentrationBC(subdomain=surf, value=0, species=H)
for surf in [
toroidal_gap,
bottom,
poloidal_gap_w,
poloidal_gap_cu,
poloidal_gap_cucrzr,
cooling_surface,
]
]
my_model.boundary_conditions = [implantation_flux_top] + recombination_fluxes
exports = {
"poloidal_gap_cu_flux": F.SurfaceFlux(surface=poloidal_gap_cu, field=H),
"poloidal_gap_cucrzr_flux": F.SurfaceFlux(surface=poloidal_gap_cucrzr, field=H),
"poloidal_gap_w_flux": F.SurfaceFlux(surface=poloidal_gap_w, field=H),
"toroidal_gap_flux": F.SurfaceFlux(surface=toroidal_gap, field=H),
"bottom_flux": F.SurfaceFlux(surface=bottom, field=H),
"inventory_w": F.TotalVolume(field=H, volume=tungsten_volume),
"inventory_cu": F.TotalVolume(field=H, volume=copper_volume),
"inventory_cucrzr": F.TotalVolume(field=H, volume=cucrzr_volume),
}
my_model.exports = list(exports.values())
my_model.settings = F.Settings(
atol=1e-8,
rtol=1e-10,
transient=False,
max_iterations=10,
)
my_model.initialise()
my_model.run()
Implantation from plasma
H = F.Species("H")
my_model.species = [H]Defining species
H = F.Species("H")
D = F.Species("D")
my_model.species = [H, D]Defining species
H = F.Species("H")
empty_traps = F.Species("empty", mobile=False)
trapped_H = F.Species("trapped_H", mobile=False)
my_model.species = [H, empty_traps, trapped_H]Adding a reaction
trapping = F.Reaction(
reactant=[H, empty_traps],
product=[trapped_H],
k_0=...,
E_k=...,
p_0=...,
E_p=...,
)
my_model.reactions = [trapping]H = F.Species("H")
empty_traps = F.Species("empty", mobile=True)
trapped_H = F.Species("trapped_H", mobile=False)
my_model.species = [H, empty_traps, trapped_H]Mobile traps?
trapping = F.Reaction(
reactant=[H, empty_traps],
product=[trapped_H],
k_0=...,
E_k=...,
p_0=...,
E_p=...,
)
my_model.reactions = [trapping]H = F.Species("H")
empty_traps = F.Species("empty", mobile=False)
empty_traps2 = F.Species("empty2", mobile=False)
trapped_H = F.Species("trapped_H", mobile=False)
trapped_H2 = F.Species("trapped_H2", mobile=False)
my_model.species = [
H, empty_traps, trapped_H,
empty_traps2, trapped_H2
]2 traps
trapping = F.Reaction(
reactant=[H, empty_traps],
product=[trapped_H],
...
)
trapping2 = F.Reaction(
reactant=[H, empty_traps2],
product=[trapped_H2],
...
)
my_model.reactions = [trapping, trapping2]H = F.Species("H")
D = F.Species("D")
empty_traps = F.Species("empty", mobile=False)
trapped_H = F.Species("trapped_H", mobile=False)
trapped_D = F.Species("trapped_D", mobile=False)
my_model.species = [
H, D, empty_traps,
trapped_H, trapped_D
]1 trap, 2 isotopes
trapping = F.Reaction(
reactant=[H, empty_traps],
product=[trapped_H],
...
)
trapping2 = F.Reaction(
reactant=[D, empty_traps],
product=[trapped_D],
...
)
my_model.reactions = [trapping, trapping2]H = F.Species("H")
empty_traps = F.Species("empty", mobile=False)
trapped_H = F.Species("trapped_H", mobile=False)
trapped_H2 = F.Species("trapped_H2", mobile=False)
my_model.species = [
H, empty_traps,
trapped_H, trapped_H2
]Multi-occupancy trapping
trapping = F.Reaction(
reactant=[H, empty_traps],
product=[trapped_H],
...
)
trapping2 = F.Reaction(
reactant=[H, empty_traps2],
product=[trapped_H2],
...
)
my_model.reactions = [trapping, trapping2]A = F.Species("A")
B = F.Species("B")
C = F.Species("C")
D = F.Species("D")
my_model.species = [A, B, C, D]"Species" can be anything...
A = F.Species("A")
B = F.Species("B")
C = F.Species("C")
D = F.Species("D")
my_model.species = [A, B, C, D]reac = F.Reaction(
reactant=[A, B],
product=[C, D],
...
)Arbitrary numbers of reactants and products
A = F.Species("A")
B = F.Species("B")
my_model.species = [A, B]reac = F.Reaction(
reactant=[A, B],
product=[],
...
)Arbitrary numbers of reactants and products
Can be used for:
H = F.Species("H")
my_model.species = [H]
reac = F.SurfaceReactionBC(
reactant=[H, H],
pressure=10,
...
)
my_model.boundary_conditions = [reac]Surface reactions
Soon: support for dynamically computed gas pressures (enclosures)
Integrate results from external solvers into FESTIM
foam2dolfinxUsing foam2dolfinx we can pass velocity fields calculated from OpenFOAM to FESTIM
foam2dolfinxfrom foam2dolfinx import OpenFOAMReader
reader = OpenFOAMReader(filename="my_foam_case.foam", cell_type=10)
# read fields
u = reader.create_dolfinx_function_with_point_data(t=100, name="U")
# read mesh
foam_mesh = reader.dolfinx_meshes_dict["default"]
# read meshtags
facet_mt = reader.create_facet_meshtags()
volume_mt = reader.create_cell_meshtags()Read a local OpenFOAM case file
Tets
Hexes
Support for:
import io4dolfinx
checkpoint_file = Path("sim_checkpoint.bp")
io4dolfinx.write_mesh(checkpoint_file, msh)
io4dolfinx.write_function(checkpoint_file, u, time=100, name="U")
io4dolfinx.write_meshtags(checkpoint_file, msh, facet_mt, meshtag_name="facet_tags")
io4dolfinx.write_meshtags(checkpoint_file, msh, cell_mt, meshtag_name="cell_tags")
..
msh = io4dolfinx.read_mesh(checkpoint_file, COMM)
facet_mt = io4dolfinx.read_meshtags(checkpoint_file, msh, meshtag_name="facet_tags")
cell_mt = io4dolfinx.read_meshtags(checkpoint_file, msh, meshtag_name="cell_tags")
gdim = msh.geometry.dim
el = basix.ufl.element("Lagrange", msh.topology.cell_name(), 1, shape=(gdim,))
w = fem.Function(fem.functionspace(msh, el), name="U")
io4dolfinx.read_function(checkpoint_file, u, time=100, name="U")...
my_model.mesh = foam_mesh
my_model.surface_meshtags = facet_meshtags
my_model.volume_meshtags = cell_meshtags
...
my_model.advection_terms = [
F.AdvectionTerm(velocity=u, species=H, subdomain=fluid)
]Pass mesh and meshtags to FESTIM
Pass velocity field using F.AdvectionTerm()
Adding advection to FESTIM means adding a term in the formulation
openmc2dolfinxUsing openmc2dolfinx we can pass fields calculated from OpenMC to FESTIM
from openmc2dolfinx import UnstructuredMeshReader, StructuredMeshReader
# UnstructuredMeshReader for tets
reader = UnstructuredMeshReader(filename="out.vtk")
# StructuredMeshReader for hexs
reader = StructuredMeshReader(filename="out.vtk")
# read fields
source_from_openmc = reader.create_dolfinx_function(name="mean")
openmc2dolfinxRead .vtk file produced by OpenMC
Tets
Hexes
Support for:
...
my_model.sources = [
F.ParticleSource(value=source_from_openmc, species=T, subdomain=volume)
]
...Input into FESTIM as a source term
Integrate FESTIM in complex workflows
1. Parametric FESTIM model
2. Produce training data
3. Train surrogate/emulator
Goal: find the trapping parameters to reproduce an experimental thermo-desorption spectrum
PathView/PathSim: a python package for system modelling
Can be seamlessly coupled with FESTIM for multi-fidelity fuel cycle simulations