Source code for festim.species
from typing import Union
import ufl
from dolfinx import fem
from festim.helpers import as_fenics_constant
from festim.subdomain.volume_subdomain import (
VolumeSubdomain as _VolumeSubdomain,
)
[docs]
class Species:
"""Hydrogen species class for H transport simulation.
Args:
name: a name given to the species. Defaults to None.
mobile: whether the species is mobile or not. Defaults to True.
subdomain: the volume subdomain where the species is. Defaults to None.
Attributes:
name: a name given to the species.
mobile: whether the species is mobile or not.
solution: the solution for the current timestep
prev_solution: the solution for the previous timestep
test_function: the testfunction associated with this species
sub_function: the sub function of the species in case of multiple species in
the same function space
sub_function_space: the subspace of the function space
collapsed_function_space: the collapsed function space for a species in the
function space. In case single species case, this is None.
map_sub_to_main_solution: the mapping from the sub solution dofs to the main
solution dofs
post_processing_solution: the solution for post processing
concentration: the concentration of the species
subdomains: the volume subdomains where the species is
subdomain_to_solution: a dictionary mapping subdomains to solutions
subdomain_to_prev_solution: a dictionary mapping subdomains to previous
solutions
subdomain_to_test_function: a dictionary mapping subdomains to test functions
subdomain_to_post_processing_solution: a dictionary mapping subdomains to post
processing solutions
subdomain_to_collapsed_function_space: a dictionary mapping subdomains to
collapsed function spaces
subdomain_to_function_space: a dictionary mapping subdomains to function spaces
Examples:
:: testsetup:: Species
from festim import Species
:: testcode:: Species
Species(name="H")
Species(name="Trap", mobile=False)
"""
name: str | None
mobile: bool
solution: fem.Function | None
prev_solution: fem.Function | None
test_function: ufl.argument.Argument | None
sub_function_space: fem.function.FunctionSpace | None
collapsed_function_space: fem.function.FunctionSpace | None
map_sub_to_main_solution: list | None
post_processing_solution: fem.Function | None
concentration: fem.Function | None
subdomains: list[_VolumeSubdomain] | _VolumeSubdomain | None
subdomain_to_solution: dict
subdomain_to_prev_solution: dict
subdomain_to_test_function: dict
subdomain_to_post_processing_solution: dict
subdomain_to_collapsed_function_space: dict
subdomain_to_function_space: dict
def __init__(
self,
name: str | None = None,
mobile: bool = True,
subdomains: list[_VolumeSubdomain] | _VolumeSubdomain | None = None,
) -> None:
self.name = name
self.mobile = mobile
self.solution = None
self.prev_solution = None
self.test_function = None
self.sub_function = None
self.sub_function_space = None
self.collapsed_function_space = None
self.map_sub_to_main_solution = None
self.post_processing_solution = None
self.subdomains = subdomains
self.subdomain_to_solution = {}
self.subdomain_to_prev_solution = {}
self.subdomain_to_test_function = {}
self.subdomain_to_post_processing_solution = {}
self.subdomain_to_collapsed_function_space = {}
self.subdomain_to_function_space = {}
def __repr__(self) -> str:
return f"Species({self.name})"
def __str__(self) -> str:
return f"{self.name}"
@property
def concentration(self):
return self.solution
def concentration_submesh(self, subdomain: _VolumeSubdomain):
assert subdomain in self.subdomains, (
f"Species {self.name} has no solution on subdomain {subdomain}."
)
return self.subdomain_to_solution[subdomain]
@property
def legacy(self) -> bool:
"""Check if we are using FESTIM 1.0 implementation or FESTIM 2.0."""
if not self.subdomain_to_solution:
return True
else:
return False
[docs]
class ImplicitSpecies:
"""Implicit species class for H transport simulation.
c = n - others
Args:
n: the total concentration of the species
others: the list of species from which the implicit species concentration is
computed (c = n - others)
name: a name given to the species. Defaults to None.
Attributes:
n: the total concentration of the species
others: the list of species from which the implicit species concentration is
computed (c = n - others)
name: a name given to the species. Defaults to None.
concentration: the concentration of the species
value_fenics: the total concentration as a fenics object
"""
n: Union[float, callable]
others: list[Species] | None
name: str | None
concentration: ufl.form.Form
value_fenics: fem.Constant | ufl.core.expr.Expr
def __init__(
self,
n: Union[float, callable],
others: list[Species] | None = None,
name: str | None = None,
) -> None:
self.name = name
self.n = n
self.others = others
def __repr__(self) -> str:
return f"ImplicitSpecies({self.name}, {self.n}, {self.others})"
def __str__(self) -> str:
return f"{self.name}"
@property
def concentration(self):
if len(self.others) > 0:
for other in self.others:
if other.solution is None:
raise ValueError(
f"Cannot compute concentration of {self.name} "
+ f"because {other.name} has no solution."
)
return self.value_fenics - sum([other.solution for other in self.others])
def concentration_submesh(self, subdomain: _VolumeSubdomain):
if len(self.others) > 0:
for other in self.others:
assert other.subdomain_to_solution[subdomain], (
f"Cannot compute concentration of {self.name} because {other.name}"
+ f" has no solution on subdomain {subdomain}."
)
return self.value_fenics - sum(
[other.subdomain_to_solution[subdomain] for other in self.others]
)
[docs]
def create_value_fenics(self, mesh, t: fem.Constant):
"""Creates the value of the density as a fenics object and sets it to
self.value_fenics. If the value is a constant, it is converted to a
fenics.Constant. If the value is a function of t, it is converted to a
fenics.Constant. Otherwise, it is converted to a ufl Expression.
Args:
mesh (dolfinx.mesh.Mesh) : the mesh
t (dolfinx.fem.Constant): the time
"""
x = ufl.SpatialCoordinate(mesh)
if isinstance(self.n, int | float):
self.value_fenics = as_fenics_constant(mesh=mesh, value=self.n)
elif isinstance(self.n, fem.Function | ufl.core.expr.Expr):
self.value_fenics = self.n
elif callable(self.n):
arguments = self.n.__code__.co_varnames
if "t" in arguments and "x" not in arguments and "T" not in arguments:
# only t is an argument
if not isinstance(self.n(t=float(t)), float | int):
raise ValueError(
"self.value should return a float or an int, not "
f"{type(self.n(t=float(t)))}"
)
self.value_fenics = as_fenics_constant(
mesh=mesh, value=self.n(t=float(t))
)
else:
kwargs = {}
if "t" in arguments:
kwargs["t"] = t
if "x" in arguments:
kwargs["x"] = x
self.value_fenics = self.n(**kwargs)
[docs]
def update_density(self, t):
"""Updates the density value (only if the density is a function of time only)
Args:
t (float): the time
"""
if isinstance(self.n, fem.Function | ufl.core.expr.Expr):
return
if callable(self.n):
arguments = self.n.__code__.co_varnames
if isinstance(self.value_fenics, fem.Constant) and "t" in arguments:
self.value_fenics.value = self.n(t=t)
[docs]
def find_species_from_name(name: str, species: list):
"""Returns the correct species object from a list of species based on a string.
Args:
name (str): the name of the species
species (list): the list of species
Returns:
species (festim.Species): the species object with the correct name
Raises:
ValueError: if the species name is not found in the list of species
"""
for spe in species:
if spe.name == name:
return spe
raise ValueError(f"Species {name} not found in list of species")