Hi All,
I am having an issue running a multi-material simulation in 2D. The model represents a square grain of ceramic (Li2TiO3) with a narrower grain adjacent to it that mimics an ‘amorphous’ layer. This layer has different material properties to the ceramic grain.
The tritium diffuses through the bulk, but when it reaches the interface with the amorphous layer it does not enter it. I wonder if it is an issue with the way I assign the F.Interface class?
Here is the code with a fig to illustrate:
import festim as F
import numpy as np
import matplotlib.pyplot as plt
import gmsh
from mpi4py import MPI
import os
from dolfinx.io import gmshio
if not os.path.exists('results'):
os.makedirs('results')
hydrogen_problem = F.HydrogenTransportProblem()
width_bulk = 10e-6 # 5 micrometers
width_amorphous = 3e-6 # 3 micrometers
def create_two_region_mesh():
"""
Create mesh using gmsh for better control over interfaces
"""
grain_size = width_bulk
gb_width = width_amorphous
gmsh.initialize()
gmsh.model.add("two_regions")
# Create points
p1 = gmsh.model.geo.addPoint(0, 0, 0)
p2 = gmsh.model.geo.addPoint(grain_size, 0, 0)
p3 = gmsh.model.geo.addPoint(grain_size, grain_size, 0)
p4 = gmsh.model.geo.addPoint(0, grain_size, 0)
p5 = gmsh.model.geo.addPoint(grain_size + gb_width, 0, 0)
p6 = gmsh.model.geo.addPoint(grain_size + gb_width, grain_size, 0)
# Create lines for grain region
l1 = gmsh.model.geo.addLine(p1, p2)
l2 = gmsh.model.geo.addLine(p2, p3)
l3 = gmsh.model.geo.addLine(p3, p4)
l4 = gmsh.model.geo.addLine(p4, p1)
# Create lines for grain boundary region
l5 = gmsh.model.geo.addLine(p2, p5)
l6 = gmsh.model.geo.addLine(p5, p6)
l7 = gmsh.model.geo.addLine(p6, p3)
# Create curve loops
grain_loop = gmsh.model.geo.addCurveLoop([l1, l2, l3, l4])
gb_loop = gmsh.model.geo.addCurveLoop([l5, l6, l7, -l2])
# Create surfaces
grain_surface = gmsh.model.geo.addPlaneSurface([grain_loop])
gb_surface = gmsh.model.geo.addPlaneSurface([gb_loop])
# Set mesh sizes
gmsh.model.geo.mesh.setSize([(0, p1), (0, p2), (0, p3), (0, p4)], grain_size/ 200) # Fine in grain
gmsh.model.geo.mesh.setSize([(0, p5), (0, p6)], gb_width/ 200) # Finer in GB
# Physical groups
gmsh.model.addPhysicalGroup(2, [grain_surface], 1)
gmsh.model.setPhysicalName(2, 1, "grain")
gmsh.model.addPhysicalGroup(2, [gb_surface], 2)
gmsh.model.setPhysicalName(2, 2, "grain_boundary")
# Boundaries
gmsh.model.addPhysicalGroup(1, [l4], 3) # left boundary
gmsh.model.setPhysicalName(1, 3, "left")
gmsh.model.addPhysicalGroup(1, [l6], 4) # right boundary
gmsh.model.setPhysicalName(1, 4, "right")
gmsh.model.addPhysicalGroup(1, [l3, l7], 5) # top boundary
gmsh.model.setPhysicalName(1, 5, "top")
gmsh.model.addPhysicalGroup(1, [l1, l5], 6) # bottom boundary
gmsh.model.setPhysicalName(1, 6, "bottom")
gmsh.model.addPhysicalGroup(1, [l2], 7) # interface
gmsh.model.setPhysicalName(1, 7, "interface")
gmsh.model.geo.synchronize()
gmsh.model.mesh.generate(2)
# Convert to dolfinx mesh
mesh_2d, cell_tags, facet_tags = gmshio.model_to_mesh(
gmsh.model, MPI.COMM_WORLD, 0, gdim=2
)
gmsh.finalize()
total_width = grain_size + gb_width
print(f"✓ GMSH two-region mesh created!")
print(f" - Total dimensions: {total_width*1e6:.1f} μm × {grain_size*1e6:.1f} μm")
return mesh_2d, total_width, grain_size, gb_width, cell_tags, facet_tags
# Update region locators for the new geometry
def bulk_grain_locator(x):
"""Square grain region (left side)"""
return x[0] < grain_size
def grain_boundary_locator(x):
"""Rectangle grain boundary region (right side)"""
return x[0] >= grain_size
# Update boundary locators for new geometry
def left_boundary_locator(x):
"""Left edge of square grain"""
return np.isclose(x[0], 0, atol=1e-12)
def right_boundary_locator(x):
"""Right edge of grain boundary"""
return np.isclose(x[0], grain_size + gb_width, atol=1e-12)
def top_boundary_locator(x):
"""Top edge (both regions)"""
return np.isclose(x[1], grain_size, atol=1e-12)
def bottom_boundary_locator(x):
"""Bottom edge (both regions)"""
return np.isclose(x[1], 0, atol=1e-12)
def gb_interface_locator(x):
"""Interface between square grain and grain boundary"""
return np.isclose(x[0], grain_size, atol=1e-12)
mesh_2d, total_width, grain_size, gb_width, cell_tags, facet_tags = create_two_region_mesh()
festim_mesh = F.Mesh(mesh_2d)
hydrogen_problem.mesh = festim_mesh
# Update materials (same as before)
bulk_material = F.Material(
name="LiTiO3",
D_0=9.5E-6,
E_D=0.80,
thermal_conductivity=2.5,
density=2881,
heat_capacity=1600
)
gb_material = F.Material(
name="LiTiO3_amorphous",
D_0=9.3e-7,
E_D=0.41,
thermal_conductivity=2.5,
density=2881,
heat_capacity=1600
)
# Volume subdomains for two regions
vol_bulk_grain = F.VolumeSubdomain(id=1, material=bulk_material, locator=bulk_grain_locator)
vol_grain_boundary = F.VolumeSubdomain(id=2, material=gb_material, locator=grain_boundary_locator)
# Surface subdomains (external boundaries only)
surf_left = F.SurfaceSubdomain(id=3, locator=left_boundary_locator)
surf_right = F.SurfaceSubdomain(id=4, locator=right_boundary_locator)
surf_top = F.SurfaceSubdomain(id=5, locator=top_boundary_locator)
surf_bottom = F.SurfaceSubdomain(id=6, locator=bottom_boundary_locator)
# Updated subdomains list
hydrogen_problem.subdomains = [
vol_bulk_grain,
vol_grain_boundary,
surf_left,
surf_right,
surf_top,
surf_bottom
]
# Interface between bulk grain and grain boundary
interface_gb = F.Interface(
id=7,
subdomains=[vol_bulk_grain, vol_grain_boundary],
penalty_term=1e25
)
hydrogen_problem.interfaces = [interface_gb]
hydrogen_problem.surface_to_volume = {
surf_left: vol_bulk_grain,
surf_right: vol_grain_boundary
}
# Species in both regions
mobile_T = F.Species("T", subdomains=[vol_bulk_grain, vol_grain_boundary])
hydrogen_problem.species = [mobile_T]
# Boundary conditions - source from left surface
boundary_left = F.FixedConcentrationBC(
subdomain=surf_left, species=mobile_T, value=1e15
)
hydrogen_problem.boundary_conditions = [boundary_left]
final_time = 100.0 # seconds
# Initial conditions
temperature = 700 # K
hydrogen_problem.temperature = temperature
hydrogen_problem.settings = F.Settings(
atol=1e10,
rtol=1e-10,
final_time=final_time,
transient=True,
max_iterations=10000
)
hydrogen_problem.settings.stepsize = F.Stepsize(1) # 1 second time step
print("Creating tritium transport problem")
# Exports for hydrogen problem
hydrogen_problem.exports = [
F.VTXSpeciesExport(
filename="results/2D/grain_level/tritium_transport/grain_tritium.bp",
field=mobile_T,
checkpoint=True,
),
F.XDMFExport(
filename="results/2D/grain_level/tritium_transport/grain_tritium.xdmf",
field=[mobile_T]
)
]
print("Individual problem exports configured")
# Run the grain problem
print("Initializing hydrogen transport simulation in 1D grains")
hydrogen_problem.initialise()
print("Running simulation...")
hydrogen_problem.run()
print("Simulation completed!")
def plot_one_grain_results():
"""
Plot results for single grain + amorphous layer geometry
"""
try:
concentration_field = F.read_function_from_file(
filename="results/2D/grain_level/tritium_transport/grain_tritium.bp",
name="T",
timestamp=final_time
)
V = concentration_field.function_space
coords = V.tabulate_dof_coordinates()
x_coords = coords[:, 0]
y_coords = coords[:, 1]
conc_values = concentration_field.x.array
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
scatter = ax.scatter(x_coords*1e6, y_coords*1e6, c=conc_values,
cmap='viridis', s=15, alpha=0.8)
ax.set_title('Tritium Concentration: Grain → Amorphous Layer', fontsize=14)
ax.set_xlabel('X Position (μm)')
ax.set_ylabel('Y Position (μm)')
ax.axvline(x=grain_size*1e6, color='red', linestyle='--', alpha=0.7,
linewidth=2, label='Grain-Amorphous Interface')
ax.legend()
ax.set_aspect('equal')
ax.grid(True, alpha=0.3)
plt.colorbar(scatter, ax=ax, label='Concentration (atoms/m³)')
plt.tight_layout()
plt.savefig('results/2D/grain_level/one_grain_results.png', dpi=300, bbox_inches='tight')
plt.show()
print(f"\n=== RESULTS ===")
print(f"Mobile tritium concentration range: {np.min(conc_values):.2e} to {np.max(conc_values):.2e} atoms/m³")
print(f"Amorphous Layer width: {gb_width*1e6:.1f} μm")
print(f"Total width: {total_width*1e6:.1f} μm")
except Exception as e:
print(f"Error in plotting: {e}")
plot_one_grain_results()
fyi I tried it in 1D first and it works:
if not os.path.exists('results'):
os.makedirs('results')
hydrogen_problem = F.HydrogenTransportProblem()
width_bulk = 10e-6 # 5 micrometers
width_amorphous = 1e-6 # 0.9 micrometers
vertices = np.concatenate(
[
np.linspace(0, width_bulk, num=600),
np.linspace(width_bulk, (width_bulk + width_amorphous), num=600)
]
)
# Create the two-grain mesh
my_mesh = F.Mesh1D(vertices=vertices)
hydrogen_problem.mesh = my_mesh
bulk_material = F.Material(
name="LiTiO3",
D_0=9.5E-6, # Diffusion pre-exponential factor [m²/s] 9.5E-6
E_D=0.80, # Activation energy [eV] 0.80
thermal_conductivity=2.5, # Thermal conductivity
density=2881, # Density [kg/m³]
heat_capacity=1600 # Heat capacity [J/kg/K]
)
gb_material = F.Material(
name="LiTiO3_amorphous",
D_0=9.3e-7,
E_D=0.41,
thermal_conductivity=2.5,
density=2881,
heat_capacity=1600
)
vol_left_grain = F.VolumeSubdomain1D(id=1, material=bulk_material, borders=[0, width_bulk])
vol_amorphous_layer = F.VolumeSubdomain1D(id=2, material=gb_material, borders=[width_bulk, (width_bulk + width_amorphous)])
left_surface = F.SurfaceSubdomain1D(id=3, x=0)
right_surface = F.SurfaceSubdomain1D(id=4, x=(width_bulk + width_amorphous))
hydrogen_problem.subdomains = [
vol_left_grain,
vol_amorphous_layer,
left_surface,
right_surface,
]
bulk_gb_interface = F.Interface(
id=5,
subdomains=[vol_left_grain, vol_amorphous_layer],
penalty_term=1e25
)
hydrogen_problem.interfaces = [bulk_gb_interface]
hydrogen_problem.surface_to_volume = {
left_surface : vol_left_grain,
right_surface : vol_amorphous_layer
}
mobile_T = F.Species("T", subdomains=[vol_left_grain, vol_amorphous_layer])
hydrogen_problem.species = [mobile_T]
hydrogen_problem.boundary_conditions = [
F.FixedConcentrationBC(
subdomain=left_surface, species=mobile_T, value=1e15
),
]
hydrogen_problem.temperature = 700 # K
hydrogen_problem.settings = F.Settings(
transient=True,
atol=1e10,
rtol=1e-10,
final_time=200,
stepsize=1,
max_iterations=10000
)
print("Creating tritium transport problem")
# Exports for hydrogen problem
hydrogen_problem.exports = [
F.VTXSpeciesExport(
filename="results/1D/grain_level/tritium_transport/bulk_tritium.bp",
field=[mobile_T],
),
F.XDMFExport(
filename="results/1D/grain_level/tritium_transport/grain_tritium.xdmf",
field=[mobile_T],
)
]
print("Individual problem exports configured")
# Run the grain problem
print("Initializing hydrogen transport simulation in 1D grains")
hydrogen_problem.initialise()
print("Running simulation...")
hydrogen_problem.run()
print("Simulation completed!")
I have also tried these other trouble shooting methods:
- a single rectangluar dolfinx mesh that is split into two regions.
- refining the mesh into the amorphous region
- using a tritium source term for the bulk instead of a fixed concentration bc on the left surface
Thank you in advance for your help!





