Skip to content

Commit d696744

Browse files
Merge pull request #904 from RemDelaporteMathurin/change-of-var-method
Change of variable method for interface discontinuities
2 parents 819379a + 22d5d79 commit d696744

File tree

12 files changed

+708
-24
lines changed

12 files changed

+708
-24
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ description = "Finite element simulations of hydrogen transport"
1313
readme = "README/md"
1414
requires-python = ">=3.9"
1515
license = { file = "LICENSE" }
16-
dependencies = ["tqdm", "scifem>=0.2.8"]
16+
dependencies = ["tqdm", "scifem>=0.2.13"]
1717
classifiers = [
1818
"Natural Language :: English",
1919
"Topic :: Scientific/Engineering",

src/festim/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
HydrogenTransportProblem,
4343
HTransportProblemPenalty,
4444
)
45+
from .problem_change_of_var import HydrogenTransportProblemDiscontinuousChangeVar
4546
from .initial_condition import InitialCondition, InitialTemperature
4647
from .material import Material
4748
from .mesh.mesh import Mesh
@@ -51,7 +52,7 @@
5152
from .reaction import Reaction
5253
from .settings import Settings
5354
from .source import HeatSource, ParticleSource, SourceBase
54-
from .species import ImplicitSpecies, Species, find_species_from_name
55+
from .species import ImplicitSpecies, Species, find_species_from_name, SpeciesChangeVar
5556
from .stepsize import Stepsize
5657
from .subdomain.interface import Interface
5758
from .subdomain.surface_subdomain import SurfaceSubdomain, find_surface_from_id

src/festim/boundary_conditions/dirichlet_bc.py

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
import ufl
66
from dolfinx import fem
77
from dolfinx import mesh as _mesh
8+
import ufl.core
9+
import ufl.core.expr
810

911
from festim import helpers
1012
from festim import subdomain as _subdomain
13+
from festim.species import Species
1114

1215

1316
class DirichletBCBase:
@@ -112,7 +115,7 @@ def define_surface_subdomain_dofs(
112115
)
113116
if mesh.topology.dim - 1 != facet_meshtags.dim:
114117
raise ValueError(
115-
f"Meshtags of dimension {facet_meshtags.dim}, expected {mesh.topology.dim-1}"
118+
f"Meshtags of dimension {facet_meshtags.dim}, expected {mesh.topology.dim - 1}"
116119
)
117120
bc_dofs = fem.locate_dofs_topological(
118121
function_space, facet_meshtags.dim, facet_meshtags.find(self.subdomain.id)
@@ -124,14 +127,16 @@ def update(self, t: float):
124127
"""Updates the boundary condition value
125128
126129
Args:
127-
t (float): the time
130+
t: the time
128131
"""
129132
if callable(self.value):
130133
arguments = self.value.__code__.co_varnames
131134
if isinstance(self.value_fenics, fem.Constant) and "t" in arguments:
132135
self.value_fenics.value = self.value(t=t)
133136
else:
134137
self.value_fenics.interpolate(self.bc_expr)
138+
elif self.bc_expr is not None:
139+
self.value_fenics.interpolate(self.bc_expr)
135140

136141

137142
class FixedConcentrationBC(DirichletBCBase):
@@ -163,13 +168,13 @@ class FixedConcentrationBC(DirichletBCBase):
163168
164169
"""
165170

166-
species: str
171+
species: Species
167172

168173
def __init__(
169174
self,
170175
subdomain: _subdomain.SurfaceSubdomain,
171176
value: np.ndarray | fem.Constant | int | float | Callable,
172-
species: str,
177+
species: Species,
173178
):
174179
self.species = species
175180
super().__init__(subdomain, value)
@@ -191,6 +196,7 @@ def create_value(
191196
function_space: fem.FunctionSpace,
192197
temperature: float | fem.Constant,
193198
t: float | fem.Constant,
199+
K_S: fem.Function = None,
194200
):
195201
"""Creates the value of the boundary condition as a fenics object and sets it to
196202
self.value_fenics.
@@ -200,9 +206,11 @@ def create_value(
200206
expression of the function is stored in `bc_expr`.
201207
202208
Args:
203-
function_space (dolfinx.fem.FunctionSpace): the function space
209+
function_space: the function space
204210
temperature: The temperature
205-
t (dolfinx.fem.Constant): the time
211+
t: the time
212+
K_S: The solubility of the species. If provided, the value of the boundary condition
213+
is divided by K_S (change of variable method).
206214
"""
207215
mesh = function_space.mesh
208216
x = ufl.SpatialCoordinate(mesh)
@@ -241,6 +249,54 @@ def create_value(
241249
)
242250
self.value_fenics.interpolate(self.bc_expr)
243251

252+
# if K_S is provided, divide the value by K_S (change of variable method)
253+
if K_S is not None:
254+
if isinstance(self.value, (int, float)):
255+
val_as_cst = helpers.as_fenics_constant(mesh=mesh, value=self.value)
256+
self.bc_expr = fem.Expression(
257+
val_as_cst / K_S,
258+
function_space.element.interpolation_points(),
259+
)
260+
self.value_fenics = fem.Function(function_space)
261+
self.value_fenics.interpolate(self.bc_expr)
262+
263+
elif callable(self.value):
264+
arguments = self.value.__code__.co_varnames
265+
266+
if "t" in arguments and "x" not in arguments and "T" not in arguments:
267+
# only t is an argument
268+
269+
# check that value returns a ufl expression
270+
if not isinstance(self.value(t=t), (ufl.core.expr.Expr)):
271+
raise ValueError(
272+
"self.value should return a ufl expression"
273+
+ f"{type(self.value(t=t))} "
274+
)
275+
276+
self.bc_expr = fem.Expression(
277+
self.value(t=t) / K_S,
278+
function_space.element.interpolation_points(),
279+
)
280+
self.value_fenics = fem.Function(function_space)
281+
self.value_fenics.interpolate(self.bc_expr)
282+
else:
283+
self.value_fenics = fem.Function(function_space)
284+
kwargs = {}
285+
if "t" in arguments:
286+
kwargs["t"] = t
287+
if "x" in arguments:
288+
kwargs["x"] = x
289+
if "T" in arguments:
290+
kwargs["T"] = temperature
291+
292+
# store the expression of the boundary condition
293+
# to update the value_fenics later
294+
self.bc_expr = fem.Expression(
295+
self.value(**kwargs) / K_S,
296+
function_space.element.interpolation_points(),
297+
)
298+
self.value_fenics.interpolate(self.bc_expr)
299+
244300

245301
# alias for FixedConcentrationBC
246302
DirichletBC = FixedConcentrationBC

src/festim/hydrogen_transport_problem.py

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import tqdm.autonotebook
99
import ufl
1010
from dolfinx import fem
11-
from scifem import NewtonSolver
11+
from scifem import BlockedNewtonSolver
1212

1313
import festim.boundary_conditions
1414
import festim.problem
@@ -167,6 +167,9 @@ def __init__(
167167
self.temperature_fenics = None
168168
self._vtxfiles: list[dolfinx.io.VTXWriter] = []
169169

170+
self._element_for_traps = "DG"
171+
self.petcs_options = petsc_options
172+
170173
@property
171174
def temperature(self):
172175
return self._temperature
@@ -463,14 +466,25 @@ def define_function_spaces(self):
463466
degree,
464467
basix.LagrangeVariant.equispaced,
465468
)
469+
element_DG = basix.ufl.element(
470+
"DG",
471+
self.mesh.mesh.basix_cell(),
472+
degree,
473+
basix.LagrangeVariant.equispaced,
474+
)
466475

467476
if not self.multispecies:
468477
element = element_CG
469478
else:
470479
elements = []
471480
for spe in self.species:
472481
if isinstance(spe, _species.Species):
473-
elements.append(element_CG)
482+
if spe.mobile:
483+
elements.append(element_CG)
484+
elif self._element_for_traps == "DG":
485+
elements.append(element_DG)
486+
else:
487+
elements.append(element_CG)
474488
element = basix.ufl.mixed_element(elements)
475489

476490
self.function_space = fem.functionspace(self.mesh.mesh, element)
@@ -730,7 +744,8 @@ def create_formulation(self):
730744
# check reactions
731745
for reaction in self.reactions:
732746
if vol == reaction.volume:
733-
not_defined_in_volume.remove(vol)
747+
if vol in not_defined_in_volume:
748+
not_defined_in_volume.remove(vol)
734749

735750
# add c = 0 to formulation where needed
736751
for vol in not_defined_in_volume:
@@ -1213,18 +1228,32 @@ def mixed_term(u, v, n):
12131228
self.forms = dolfinx.fem.form(
12141229
[subdomain.F for subdomain in self.volume_subdomains],
12151230
entity_maps=entity_maps,
1231+
jit_options={
1232+
"cffi_extra_compile_args": ["-O3", "-march=native"],
1233+
"cffi_libraries": ["m"],
1234+
},
1235+
)
1236+
self.J = dolfinx.fem.form(
1237+
J,
1238+
entity_maps=entity_maps,
1239+
jit_options={
1240+
"cffi_extra_compile_args": ["-O3", "-march=native"],
1241+
"cffi_libraries": ["m"],
1242+
},
12161243
)
1217-
self.J = dolfinx.fem.form(J, entity_maps=entity_maps)
12181244

12191245
def create_solver(self):
1220-
self.solver = NewtonSolver(
1246+
self.solver = BlockedNewtonSolver(
12211247
self.forms,
1222-
self.J,
12231248
[subdomain.u for subdomain in self.volume_subdomains],
1249+
J=self.J,
12241250
bcs=self.bc_forms,
1225-
max_iterations=self.settings.max_iterations,
12261251
petsc_options=self.petsc_options,
12271252
)
1253+
self.solver.max_iterations = self.settings.max_iterations
1254+
self.solver.convergence_criterion = self.settings.convergence_criterion
1255+
self.solver.atol = self.settings.atol
1256+
self.solver.rtol = self.settings.rtol
12281257

12291258
def create_flux_values_fenics(self):
12301259
"""For each particle flux create the ``value_fenics`` attribute"""
@@ -1283,8 +1312,8 @@ def iterate(self):
12831312

12841313
self.update_time_dependent_values()
12851314

1286-
# solve main problem
1287-
self.solver.solve(self.settings.atol, self.settings.rtol)
1315+
# Solve main problem
1316+
self.solver.solve()
12881317

12891318
# post processing
12901319
self.post_processing()
@@ -1312,7 +1341,7 @@ def run(self):
13121341
self.progress_bar.refresh() # refresh progress bar to show 100%
13131342
else:
13141343
# Solve steady-state
1315-
self.solver.solve(self.settings.rtol)
1344+
self.solver.solve()
13161345
self.post_processing()
13171346

13181347
def __del__(self):

src/festim/material.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,58 @@ def get_E_D(self, species=None):
121121
else:
122122
raise TypeError("E_D must be either a float, int or a dict")
123123

124+
def get_K_S_0(self, species=None) -> float:
125+
"""Returns the pre-exponential factor of the solubility coefficient
126+
Args:
127+
species: the species we want the pre-exponential
128+
factor of the solubility coefficient of. Only needed if K_S_0 is a dict.
129+
Returns:
130+
the pre-exponential factor of the solubility coefficient
131+
"""
132+
133+
if isinstance(self.K_S_0, (float, int)):
134+
return self.K_S_0
135+
136+
elif isinstance(self.K_S_0, dict):
137+
if species is None:
138+
raise ValueError("species must be provided if K_S_0 is a dict")
139+
140+
if species in self.K_S_0:
141+
return self.K_S_0[species]
142+
elif species.name in self.K_S_0:
143+
return self.K_S_0[species.name]
144+
else:
145+
raise ValueError(f"{species} is not in K_S_0 keys")
146+
147+
else:
148+
raise TypeError("K_S_0 must be either a float, int or a dict")
149+
150+
def get_E_K_S(self, species=None) -> float:
151+
"""Returns the activation energy of the solubility coefficient
152+
Args:
153+
species: the species we want the activation
154+
energy of the solubility coefficient of. Only needed if E_K_S is a dict.
155+
Returns:
156+
the activation energy of the solubility coefficient
157+
"""
158+
159+
if isinstance(self.E_K_S, (float, int)):
160+
return self.E_K_S
161+
162+
elif isinstance(self.E_K_S, dict):
163+
if species is None:
164+
raise ValueError("species must be provided if E_K_S is a dict")
165+
166+
if species in self.E_K_S:
167+
return self.E_K_S[species]
168+
elif species.name in self.E_K_S:
169+
return self.E_K_S[species.name]
170+
else:
171+
raise ValueError(f"{species} is not in E_K_S keys")
172+
173+
else:
174+
raise TypeError("E_K_S must be either a float, int or a dict")
175+
124176
def get_diffusion_coefficient(self, mesh, temperature, species=None):
125177
"""Defines the diffusion coefficient
126178
Args:
@@ -184,7 +236,7 @@ def get_solubility_coefficient(self, mesh, temperature, species=None):
184236
Returns:
185237
ufl.algebra.Product: the solubility coefficient
186238
"""
187-
# TODO use get_D_0 and get_E_D to refactore this method, something like:
239+
# TODO use get_K_S0 and get_E_K_S to refactore this method, something like:
188240
# K_S_0 = self.get_K_S_0(species=species)
189241
# E_K_S = self.get_E_K_S(species=species)
190242

0 commit comments

Comments
 (0)