Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
031620a
new interface residual export
jhdark Jun 2, 2026
ecb87a5
add to inits
jhdark Jun 2, 2026
54d355b
export initialisation and writing
jhdark Jun 2, 2026
c79f043
evaluate residual
jhdark Jun 2, 2026
1ec3086
additional details in the doc strings
jhdark Jun 2, 2026
c5e5f79
remove print statement
jhdark Jun 2, 2026
eb9e212
make function for getting interface term
jhdark Jun 2, 2026
a00a935
use new interface function
jhdark Jun 2, 2026
359b16a
update doc strings, showing residual depending on interface condition
jhdark Jun 2, 2026
fc52f67
use dolin expression instead
jhdark Jun 22, 2026
ac1186f
refactoring and export consituents of the residual
jhdark Jun 22, 2026
7571515
refactoring
jhdark Jun 22, 2026
006e4c3
all residuals + ufl instead of np
RemDelaporteMathurin Jun 23, 2026
523372e
export f0 and f1 instead of c0 and c1
RemDelaporteMathurin Jun 23, 2026
a54ec6c
Merge branch 'main' into interface_condition_diagnostic
RemDelaporteMathurin Jun 23, 2026
9718de1
Merge branch 'main' into interface_condition_diagnostic
jhdark Jul 7, 2026
bc0aeb4
correct residul, fix doc string errors
jhdark Jul 7, 2026
3d08bb1
added tests
jhdark Jul 7, 2026
e18b191
make temperature a function
jhdark Jul 7, 2026
b640e05
Merge branch 'main' into interface_condition_diagnostic
jhdark Jul 21, 2026
813c140
Merge branch 'main' into interface_condition_diagnostic
jhdark Jul 21, 2026
fa96786
Merge branch 'main' into interface_condition_diagnostic
jhdark Jul 27, 2026
842e4bc
k_B, interpolation ponts fix
jhdark Jul 28, 2026
5c7617e
Merge branch 'main' into interface_condition_diagnostic
jhdark Aug 12, 2026
58e4dd8
ignore local claude files
jhdark Aug 12, 2026
c12aff7
changes based on code review
jhdark Aug 12, 2026
dd3247d
wont work with trad H transport problem class
jhdark Aug 12, 2026
040d97e
updated tests
jhdark Aug 12, 2026
c17b054
fix: honour the method set on each Interface
jhdark Aug 12, 2026
aa1912b
fix: apply solubility laws to the Nitsche interface jump
jhdark Aug 12, 2026
b52f652
docs: export interface_condition_term from festim.subdomain
jhdark Aug 12, 2026
75d3a0f
docs: record why the residual export computes K_S itself
jhdark Aug 12, 2026
b7af562
test: cover both orderings of the mixed solubility laws
jhdark Aug 12, 2026
8d4d8c5
test: verify the mixed-law Nitsche scheme scales
jhdark Aug 12, 2026
8705fff
test: pin the mixed-law comparison at an adequate penalty
jhdark Aug 12, 2026
f1bd802
fix: evaluate the residual against a uniform temperature
jhdark Aug 12, 2026
5921b3e
test: cover the transient path
jhdark Aug 12, 2026
cc60496
print residuals at each iteration when export given
jhdark Aug 13, 2026
f07a7b9
dont allow mixed law with nitsche
jhdark Aug 13, 2026
b69594a
support for 1D
jhdark Aug 13, 2026
71b98e7
filename optional
jhdark Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/festim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
CustomFieldExport,
ExportBaseClass,
ReactionRateExport,
VTXInterfaceResidualExport,
VTXSpeciesExport,
VTXTemperatureExport,
)
Expand Down
2 changes: 2 additions & 0 deletions src/festim/exports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
CustomFieldExport,
ExportBaseClass,
ReactionRateExport,
VTXInterfaceResidualExport,
VTXSpeciesExport,
VTXTemperatureExport,
)
Expand All @@ -38,6 +39,7 @@
"SurfaceQuantity",
"TotalSurface",
"TotalVolume",
"VTXInterfaceResidualExport",
"VTXSpeciesExport",
"VTXTemperatureExport",
"VolumeQuantity",
Expand Down
178 changes: 177 additions & 1 deletion src/festim/exports/vtx.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
from pathlib import Path
from typing import Union

import dolfinx
import numpy as np
import ufl
from dolfinx import fem, io

from festim import k_B as _k_B
from festim.helpers import get_interpolation_points
from festim.reaction import Reaction
from festim.species import ImplicitSpecies, Species
from festim.subdomain.interface import Interface, interface_condition_term
from festim.subdomain.volume_subdomain import VolumeSubdomain


Expand Down Expand Up @@ -345,6 +348,180 @@ def check_valid_inputs(self, kwargs: dict):
)


class VTXInterfaceResidualExport(ExportBaseClass):
"""Export the interface condition residual to a VTX file.

This quantity measures how well the penalty/Nitsche interface condition is
satisfied at the interface. It is zero when the condition holds exactly; the lower
the value, the better.

The residual is ``right - left`` where each side's term depends on the
solubility laws of the two subdomains:

- **Same law on both sides** (Henry-Henry or Sievert-Sievert):
``residual = c_1/K_S_1 - c_0/K_S_0``
- **Henry (side 0) - Sievert (side 1)**:
``residual = (c_1/K_S_1)^2 - c_0/K_S_0``
- **Sievert (side 0) - Henry (side 1)**:
``residual = c_1/K_S_1 - (c_0/K_S_0)^2``

Args:
field: The species whose interface residual is exported.
filename: The name of the output file.
interface: The interface between the two subdomains.
times: if provided, the field will be exported at these timesteps.
Otherwise exports at all timesteps. Defaults to None.

Attributes:
field: The species to export.
filename: The name of the output file.
interface: The interface between the two subdomains.
function: Residual function on the interface submesh. Set by
``initialise``.
writer: VTXWriter used to write the output file. Set by ``initialise``.
"""

field: Species
interface: Interface
times: list[float] | list[int] | None

function: fem.Function
writer: io.VTXWriter

def __init__(
self,
field: Species,
filename: str | Path,
interface: Interface,
times: list[float | int] | None = None,
):
super().__init__(filename, ".bp", times)
self.field = field
self.interface = interface

def initialise(self, temperature_fenics: fem.Constant | fem.Function) -> None:
"""Create the interface submesh, interpolation data, and VTX writer.

Called by the problem during ``initialise_exports``. Builds a CG1
function space on the interface submesh, pre-computes
``create_interpolation_data`` for both subdomain concentrations and
(if space-dependent) temperature, and opens the VTXWriter.

Args:
temperature_fenics: Temperature field on the parent mesh. Either a
``fem.Constant`` (uniform) or a ``fem.Function`` (spatially
varying).
"""
parent_mesh = self.interface.parent_mesh
fdim = parent_mesh.topology.dim - 1
interface_facets = self.interface.mt.find(self.interface.id)

interface_submesh, _, _, _ = dolfinx.mesh.create_submesh(
parent_mesh, fdim, interface_facets
)
V_interface = fem.functionspace(interface_submesh, ("CG", 1))

self._u_0 = fem.Function(V_interface)
self._u_1 = fem.Function(V_interface)
self.function = fem.Function(V_interface)
self.function.name = f"{self.field.name}_interface_residual"

imap = interface_submesh.topology.index_map(interface_submesh.topology.dim)
self._interface_cells = np.arange(
imap.size_local + imap.num_ghosts, dtype=np.int32
)

subdomain_0, subdomain_1 = self.interface.subdomains
V_0 = self.field.subdomain_to_post_processing_solution[
subdomain_0
].function_space
V_1 = self.field.subdomain_to_post_processing_solution[
subdomain_1
].function_space

self._interp_data_0 = fem.create_interpolation_data(
V_interface, V_0, self._interface_cells, padding=1e-11
)
self._interp_data_1 = fem.create_interpolation_data(
V_interface, V_1, self._interface_cells, padding=1e-11
)

self._K_S_0 = subdomain_0.material.get_K_S_0(self.field)
self._E_K_S_0 = subdomain_0.material.get_E_K_S(self.field)
self._K_S_1 = subdomain_1.material.get_K_S_0(self.field)
self._E_K_S_1 = subdomain_1.material.get_E_K_S(self.field)
self._law_0 = subdomain_0.material.solubility_law
self._law_1 = subdomain_1.material.solubility_law

self._temperature_fenics = temperature_fenics
if isinstance(temperature_fenics, fem.Constant):
self._T_func = None
else:
self._T_func = fem.Function(V_interface)
self._T_interp_data = fem.create_interpolation_data(
V_interface,
temperature_fenics.function_space,
self._interface_cells,
padding=1e-11,
)

self.writer = io.VTXWriter(
comm=interface_submesh.comm,
filename=self.filename,
output=self.function,
engine="BP5",
)

def write(self, t: float) -> None:
"""Compute the interface condition residual and write to file.

Interpolates the concentration from each subdomain onto the interface
submesh, evaluates ``K_S = K_S_0 * exp(-E_K_S / (k_B * T))`` at the
current temperature, then computes ``right - left`` via
:func:`interface_condition_term`:

- **Same law or Henry (side 0)**: ``left = c_0 / K_S_0``
- **Sievert (side 0, mixed only)**: ``left = (c_0 / K_S_0)^2``

and symmetrically for ``right``.

Args:
t: Current simulation time.
"""
subdomain_0, subdomain_1 = self.interface.subdomains
u_0 = self.field.subdomain_to_post_processing_solution[subdomain_0]
u_1 = self.field.subdomain_to_post_processing_solution[subdomain_1]

self._u_0.interpolate_nonmatching(
u_0, self._interface_cells, interpolation_data=self._interp_data_0
)
self._u_1.interpolate_nonmatching(
u_1, self._interface_cells, interpolation_data=self._interp_data_1
)

if self._T_func is not None:
self._T_func.interpolate_nonmatching(
self._temperature_fenics,
self._interface_cells,
interpolation_data=self._T_interp_data,
)
T = self._T_func.x.array
else:
T = float(self._temperature_fenics)

K_S_0 = self._K_S_0 * np.exp(-self._E_K_S_0 / (_k_B * T))
K_S_1 = self._K_S_1 * np.exp(-self._E_K_S_1 / (_k_B * T))
left = interface_condition_term(
self._u_0.x.array, K_S_0, self._law_0, self._law_1
)
right = interface_condition_term(
self._u_1.x.array, K_S_1, self._law_1, self._law_0
)
self.function.x.array[:] = right - left

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think using .x.array for this might be risky since it assumes they ordered wrt dofs in the exact same way, which might not be True.

the proper way to do it is to make a fem.Expression object with u_0/K_s - u_1/K_s and then interpolate it in self.function


self.writer.write(t)


class ReactionRateExport(CustomFieldExport):
"""Export a reaction rate to a VTX file

Expand All @@ -370,7 +547,6 @@ def __init__(
subdomain: VolumeSubdomain | None = None,
checkpoint: bool = False,
):

reactant_names = [reactant.name for reactant in reaction.reactant]
if isinstance(reaction.product, list):
product_names = [product.name for product in reaction.product]
Expand Down
5 changes: 5 additions & 0 deletions src/festim/hydrogen_transport_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1738,6 +1738,9 @@ def initialise_exports(self):
self.temperature_fenics,
engine="BP5",
)
elif isinstance(export, exports.VTXInterfaceResidualExport):
export.initialise(self.temperature_fenics)

elif isinstance(export, exports.CustomFieldExport):
# need to find an appropriate function space on the right submesh
V = self.subdomain_to_V_CG1[export.subdomain]
Expand Down Expand Up @@ -1853,6 +1856,8 @@ def post_processing(self):
export.writer.write(float(self.t))
elif isinstance(export, exports.VTXTemperatureExport):
export.writer.write(float(self.t))
elif isinstance(export, exports.VTXInterfaceResidualExport):
export.write(float(self.t))
else:
raise NotImplementedError(
f"Export type {type(export)} not implemented"
Expand Down
67 changes: 40 additions & 27 deletions src/festim/subdomain/interface.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from abc import ABC, abstractmethod
from enum import Enum
from typing import TYPE_CHECKING

import dolfinx
import numpy as np
import numpy.typing as npt
import ufl
from dolfinx.cpp.fem import compute_integration_domains
from packaging.version import Version
Expand All @@ -13,7 +15,38 @@
if TYPE_CHECKING:
from festim.species import Species

from abc import ABC, abstractmethod

def interface_condition_term(
c: ufl.core.expr.Expr | npt.NDArray[np.floating],
K_S: ufl.core.expr.Expr | npt.NDArray[np.floating],
law: SolubilityLaw,
other_law: SolubilityLaw,
) -> ufl.core.expr.Expr | npt.NDArray[np.floating]:
"""Return the interface condition expression for one side.

When both sides share the same solubility law the condition simplifies to
``c/K_S`` on each side. When the laws differ Henry is expressed as
``c/K_S`` and Sievert as ``(c/K_S)^2``.

Args:
c: Concentration on this side.
K_S: Solubility coefficient on this side.
law: Solubility law for this side.
other_law: Solubility law for the other side.

Returns:
The interface condition term for this side.

Raises:
ValueError: If ``law`` is not ``HENRY`` or ``SIEVERT`` in the mixed-law
case.
"""
if law == other_law or law == SolubilityLaw.HENRY:
return c / K_S
elif law == SolubilityLaw.SIEVERT:
return (c / K_S) ** 2
else:
raise ValueError(f"Unsupported solubility law: {law}")


class InterfaceMethod(Enum):
Expand Down Expand Up @@ -393,32 +426,12 @@ def penalty_method(self, dS, species, temperature):
u_0, u_1 = self.us(species)
v_0, v_1 = self.vs(species)
K_0, K_1 = self.Ks(species, temperature)
if subdomain_0.material.solubility_law == subdomain_1.material.solubility_law:
left = u_0 / K_0
right = u_1 / K_1
else:
match subdomain_0.material.solubility_law:
case SolubilityLaw.HENRY:
left = u_0 / K_0
case SolubilityLaw.SIEVERT:
left = (u_0 / K_0) ** 2
case _:
raise ValueError(
"Unsupported material law "
+ f"{subdomain_0.material.solubility_law}"
)

match subdomain_1.material.solubility_law:
case SolubilityLaw.HENRY:
right = u_1 / K_1
case SolubilityLaw.SIEVERT:
right = (u_1 / K_1) ** 2
case _:
raise ValueError(
f"Unsupported material law "
f"{subdomain_1.material.solubility_law}"
)

law_0, law_1 = (
subdomain_0.material.solubility_law,
subdomain_1.material.solubility_law,
)
left = interface_condition_term(u_0, K_0, law_0, law_1)
right = interface_condition_term(u_1, K_1, law_1, law_0)
equality = right - left

F_0 = self.penalty_term * ufl.inner(equality, v_0) * dS(self.id)
Expand Down
Loading