Hello everyone,
I’m new to FESTIM and currently preparing some Tritium transport modelling work. Before starting, I discussed good practices with colleagues experienced in hydrogen transport (mostly using OpenFOAM), and they recommended regularly checking mass conservation. I wanted to implement this in FESTIM to build confidence in my setup.
To check mass balance for steady-state cases, I compared the hydrogen fluxes across all boundaries (Inflow - Outflow). When I used a volumetric source term, I added the generated hydrogen to the inflow side. I obtained the boundary fluxes using HydrogenFlux via DerivedQuantities.
In several cases, my mass-balance check suggests that the total fluxes do not match. Since I’m still new to FESTIM, I’m unsure whether my way of calculating the balance is complete, whether I am missing a detail in how fluxes should be interpreted, or whether there are limitations in how fluxes are currently exported. I would really appreciate some guidance from more experienced users.
Here is how I computed the relevant quantities:
# Before running the simulation
Flux_wall_l = F.HydrogenFlux(surface=wall_l_id)
Flux_wall_r = F.HydrogenFlux(surface=wall_r_id)
Flux_outlet = F.HydrogenFlux(surface=outlet_id)
Flux_inlet = F.HydrogenFlux(surface=inlet_id)
derived_quantities = F.DerivedQuantities(
[Flux_wall_l, Flux_wall_r, Flux_outlet, Flux_inlet],
show_units=True
)
my_sim.exports = [derived_quantities]
# After running the simulation
Difference = (Flux_wall_r.data[0]
+ Flux_outlet.data[0]
+ Flux_inlet.data[0]
+ Flux_wall_l.data[0]
+ Source)
Relative = Difference / (Flux_inlet.data[0] + Source)
print(Difference)
print(f'Relative difference = {Relative:.2%}')
For simple 2D Cartesian cases with Dirichlet-zero walls, flux conditions at the inlet, and/or a source term, the mass balance looks consistent.
However, when I switch to a cylindrical (axisymmetric) mesh and remove the Dirichlet BC on the wall representing the symmetry axis, I see a significant discrepancy. Just to clarify my interpretation: in my mesh, wall_l corresponds to the axis, so there should be no net mass transfer across it. Anything crossing the axis should remain inside the domain. Even with that understanding, I still observe that the inflow is ~50% higher than the outflow in these axisymmetric cases.
When I add an advection term to the transport equation, the discrepancy becomes even larger and appears to scale with the advection velocity (higher velocity → larger mismatch). This makes me suspect that I may be overlooking some detail about how fluxes should be combined or interpreted in FESTIM, especially in combination with advection.
I would be very grateful if someone could help me understand whether I am performing the mass balance correctly, or if there is something important I might be missing in this kind of setup.
Below is a minimal script that reproduces the behaviour I described.
Thanks a lot in advance for your help,
Chris
import festim as F
import numpy as np
from fenics import UnitSquareMesh, CompiledSubDomain, MeshFunction, plot, RectangleMesh, Point
import matplotlib.pyplot as plt
def create_mesh2D_rec(inlet_id,outlet_id,wall_left_id,wall_right_id, r_max, z_max, r_min=0, z_min=0,nx=40, ny=200):
"""Create a mesh for an annular flow mass exhanger
Args:
inlet_id
outlet_id
wall_id
axis_id
r_max
z_max
r_min
z_min
"""
# Create a mesh with fenix
mesh_fenics = RectangleMesh(Point(r_min, z_min), Point(r_max, z_max), nx, ny)
# --- Ensure topology is initialized ---
mesh_fenics.init(mesh_fenics.topology().dim() - 1)
# marking the physical groups (volumes and surfaces)
volume_markers = MeshFunction("size_t", mesh_fenics, mesh_fenics.topology().dim())
volume_markers.set_all(1)
tol = 1e-14
inlet_surface = CompiledSubDomain('on_boundary && near(x[1],z_min,tol)',tol=tol, z_min=z_min)
outlet_surface = CompiledSubDomain('on_boundary && near(x[1], z_max , tol)', tol=tol, z_max = z_max)
wall_right_surface = CompiledSubDomain('on_boundary && near(x[0], r_max, tol)', tol=tol, r_max = r_max)
wall_left_surface = CompiledSubDomain('on_boundary && near(x[0], r_min, tol)', tol=tol, r_min = r_min)
surface_markers = MeshFunction("size_t", mesh_fenics, mesh_fenics.topology().dim() - 1)
surface_markers.set_all(0)
inlet_surface.mark(surface_markers, inlet_id)
outlet_surface.mark(surface_markers, outlet_id)
wall_right_surface.mark(surface_markers, wall_right_id)
wall_left_surface.mark(surface_markers, wall_left_id)
plot(mesh_fenics)
return mesh_fenics, volume_markers, surface_markers
inner_radius = 0
outer_radius = 1
length = 1
inlet_id = 1
outlet_id = 2
wall_r_id = 3
wall_l_id = 4
my_sim = F.Simulation()
mesh, volume_markers, surface_markers = create_mesh2D_rec(inlet_id=inlet_id,outlet_id=outlet_id
,wall_right_id=wall_r_id,wall_left_id = wall_l_id,
r_max=outer_radius, z_max=length,
nx=500,ny=500)
my_sim.mesh = F.Mesh(
mesh = mesh,
volume_markers=volume_markers,
surface_markers=surface_markers,
#type="cylindrical",
)
my_sim.materials = F.Material(id=1, D_0=1, E_D=0)
my_sim.T = 500
my_sim.boundary_conditions = [
F.DirichletBC(field=0, surfaces=wall_r_id, value=0),
F.DirichletBC(field=0, surfaces=wall_l_id, value=0),
F.FluxBC(field=0, surfaces=inlet_id, value=1e17)
]
my_sim.settings = F.Settings(
absolute_tolerance=1e-10,
relative_tolerance=1e-10,
transient=False
)
Flux_wall_l = F.HydrogenFlux(surface=wall_l_id)
Flux_wall_r = F.HydrogenFlux(surface=wall_r_id)
Flux_outlet = F.HydrogenFlux(surface=outlet_id)
Flux_inlet = F.HydrogenFlux(surface=inlet_id)
derived_quantities = F.DerivedQuantities(
[Flux_wall_l,Flux_wall_r,Flux_outlet,Flux_inlet],
show_units=True)
my_sim.exports = [derived_quantities]
my_sim.initialise()
my_sim.run()
hydrogen_concentration = my_sim.h_transport_problem.mobile.solution
plt.figure()
cs=plot(hydrogen_concentration)
plt.colorbar(cs)
plt.show()
Difference_sym= Flux_wall_r.data[0] + Flux_outlet.data[0] + Flux_inlet.data[0] + Flux_wall_l.data[0]
Relative_sym = Difference_sym/Flux_inlet.data[0]
print(Difference_sym)
print(f'Relative difference = {Relative_sym:.2%}')
print(Flux_inlet.data[0])
print(Flux_wall_r.data[0])
print(Flux_wall_l.data[0])
print(Flux_outlet.data[0])
from fenics import interpolate, Expression, VectorFunctionSpace
functionspace = VectorFunctionSpace(my_sim.mesh.mesh, "CG", 1)
velocity = Expression(("0", "4*v/pow(R,2)*x[0]*(R-x[0])"),v=20,R=outer_radius,degree=2)
velocity = interpolate(velocity, functionspace)
CS = plot(velocity)
plt.colorbar(CS)
plt.show()
Flux_wall_l = F.HydrogenFlux(surface=wall_l_id)
Flux_wall_r = F.HydrogenFlux(surface=wall_r_id)
Flux_outlet = F.HydrogenFlux(surface=outlet_id)
Flux_inlet = F.HydrogenFlux(surface=inlet_id)
derived_quantities = F.DerivedQuantities(
[Flux_wall_l,Flux_wall_r,Flux_outlet,Flux_inlet],
show_units=True)
my_sim.exports = [derived_quantities]
from fenics import inner, dot, grad
my_sim.initialise()
hydrogen_concentration = my_sim.h_transport_problem.mobile.solution
test_function_solute = my_sim.h_transport_problem.mobile.test_function
advection_term = inner(dot(grad(hydrogen_concentration),velocity),test_function_solute)*my_sim.mesh.dx
my_sim.h_transport_problem.F += advection_term
my_sim.run()
ch = plot(hydrogen_concentration)
plt.colorbar(ch)
plt.show()
Difference_sym= Flux_wall_r.data[0] + Flux_outlet.data[0] + Flux_inlet.data[0] + Flux_wall_l.data[0]
Relative_sym = Difference_sym/Flux_inlet.data[0]
print(Difference_sym)
print(f'Relative difference = {Relative_sym:.2%}')
print(Flux_inlet.data[0])
print(Flux_wall_r.data[0])
print(Flux_wall_l.data[0])
print(Flux_outlet.data[0])