from operator import itemgetter
import warnings
import numpy as np
from festim import k_B, Material, HeatTransferProblem
import festim
import fenics as f
from typing import Union
[docs]
class Materials(list):
"""
A list of festim.Material objects
"""
def __init__(self, *args):
# checks that input is list
if len(args) == 0:
super().__init__()
else:
if not isinstance(*args, list):
raise TypeError("festim.Materials must be a list")
super().__init__(self._validate_material(item) for item in args[0])
self.D = None
self.S = None
self.thermal_cond = None
self.heat_capacity = None
self.density = None
self.Q = None
def __setitem__(self, index, item):
super().__setitem__(index, self._validate_material(item))
[docs]
def insert(self, index, item):
super().insert(index, self._validate_material(item))
[docs]
def append(self, item):
super().append(self._validate_material(item))
[docs]
def extend(self, other):
if isinstance(other, type(self)):
super().extend(other)
else:
super().extend(self._validate_material(item) for item in other)
def _validate_material(self, value):
if isinstance(value, festim.Material):
return value
raise TypeError("festim.Materials must be a list of festim.Material")
[docs]
def check_borders(self, size):
"""Checks that the borders of the materials match
Args:
size (float): size of the 1D domain
Raises:
ValueError: if the borders don't begin at zero
ValueError: if borders don't match
ValueError: if borders don't end at size
Returns:
bool -- True if everything's alright
"""
all_borders = []
for m in self:
if isinstance(m.borders[0], list):
for border in m.borders:
all_borders.append(border)
else:
all_borders.append(m.borders)
all_borders = sorted(all_borders, key=itemgetter(0))
if all_borders[0][0] != 0:
raise ValueError("Borders don't begin at zero")
for i in range(0, len(all_borders) - 1):
if all_borders[i][1] != all_borders[i + 1][0]:
raise ValueError("Borders don't match to each other")
if all_borders[len(all_borders) - 1][1] != size:
raise ValueError("Borders don't match with size")
return True
[docs]
def check_materials(self, T: festim.Temperature, derived_quantities: list = []):
"""Checks the materials keys
Args:
T (festim.Temperature): the temperature
derived_quantities (list): list of festim.DerivedQuantity
objects the derived quantities. Defaults to [].
"""
if len(self) > 0: # TODO: get rid of this...
self.check_consistency()
self.check_for_unused_properties(T, derived_quantities)
self.check_unique_ids()
self.check_missing_properties(T, derived_quantities)
def check_unique_ids(self):
# check that ids are different
mat_ids = []
for mat in self:
if type(mat.id) is list:
mat_ids += mat.id
else:
mat_ids.append(mat.id)
if len(mat_ids) != len(np.unique(mat_ids)):
raise ValueError("Some materials have the same id")
[docs]
def check_for_unused_properties(
self, T: festim.Temperature, derived_quantities: list
):
"""Warns users if properties will be ignored
Args:
T (festim.Temperature): the temperature
derived_quantities (list): list of festim.DerivedQuantity
objects
"""
# TODO add a check for ignored solubility when chemical_pot is False
# warn about unused keys
transient_properties = ["rho", "heat_capacity"]
if not isinstance(T, HeatTransferProblem):
for mat in self:
for key in transient_properties:
if getattr(mat, key) is not None:
warnings.warn(key + " key will be ignored", UserWarning)
for mat in self:
if getattr(mat, "thermal_cond") is not None:
warn = True
if isinstance(T, HeatTransferProblem):
warn = False
else:
surface_fluxes = list(
quant
for quant in derived_quantities
if isinstance(quant, festim.SurfaceFlux)
)
for surface_flux in surface_fluxes:
if surface_flux.field == "T":
warn = False
if warn:
warnings.warn("thermal_cond key will be ignored", UserWarning)
[docs]
def check_consistency(self):
"""Checks that materials have the same attributes"""
# check the materials keys match
attributes = {
"S_0": [],
"E_S": [],
"thermal_cond": [],
"heat_capacity": [],
"rho": [],
"borders": [],
"Q": [],
}
for attr, value in attributes.items():
for mat in self:
value.append(getattr(mat, attr))
if value.count(None) not in [0, len(self)]:
raise ValueError("{} is not defined for all materials".format(attr))
[docs]
def check_missing_properties(self, T: festim.Temperature, derived_quantities: list):
"""Checks if the materials miss some properties
Args:
T (festim.Temperature): the temperature
derived_quantities (list): list of festim.DerivedQuantity objects
Raises:
ValueError: if thermal_cond, heat_capacity or rho is None when needed
"""
if isinstance(T, HeatTransferProblem):
if self[0].thermal_cond is None:
raise ValueError("Missing thermal_cond in materials")
if T.transient:
if self[0].heat_capacity is None:
raise ValueError("Missing heat_capacity in materials")
if self[0].rho is None:
raise ValueError("Missing rho in materials")
# TODO: add check for thermal cond for thermal flux computation
[docs]
def find_material_from_id(self, mat_id):
"""Returns the material from a given id
Args:
mat_id (int): id of the wanted material
Raises:
ValueError: if the id isn't found
Returns:
festim.Material: the material that has the id mat_id
"""
for material in self:
mat_ids = material.id
if type(mat_ids) is not list:
mat_ids = [mat_ids]
if mat_id in mat_ids:
return material
raise ValueError("Couldn't find ID " + str(mat_id) + " in materials list")
[docs]
def find_material_from_name(self, name):
"""Returns the material with the correct name
Args:
name (str): the name of the material
Raises:
ValueError: when no match was found
Returns:
festim.Material: the material object
"""
for material in self:
if material.name == name:
return material
msg = "No material with name {} was found".format(name)
raise ValueError(msg)
[docs]
def find_material(self, mat: Union[int, str, Material]):
"""Returns the correct festim.Material object based on either an id,
a name
Args:
mat (Union[int, str, Material]): the material tag
Returns:
festim.Material: the matching material
"""
if isinstance(mat, int):
return self.find_material_from_id(mat)
elif isinstance(mat, str):
return self.find_material_from_name(mat)
elif isinstance(mat, Material):
return mat
[docs]
def find_subdomain_from_x_coordinate(self, x):
"""Finds the correct subdomain at a given x coordinate
Args:
x (float): the x coordinate
Returns:
int: the corresponding subdomain id
"""
for material in self:
# if no borders are provided, assume only one subdomain
if material.borders is None:
return material.id
# else find the correct material
else:
if isinstance(material.borders[0], list) and len(material.borders) > 1:
list_of_borders = material.borders
else:
list_of_borders = [material.borders]
if isinstance(material.id, list):
subdomains = material.id
else:
subdomains = [material.id for _ in range(len(list_of_borders))]
for borders, subdomain in zip(list_of_borders, subdomains):
if borders[0] <= x <= borders[1]:
return subdomain
# if no subdomain was found, return 0
return 0
[docs]
def create_properties(self, vm, T):
"""Creates the properties fields needed for post processing
Arguments:
vm {fenics.MeshFunction()} -- volume markers
T {fenics.Function()} -- temperature
"""
self.D = ArheniusCoeff(self, vm, T, "D_0", "E_D", degree=2)
# all materials have the same properties so only checking the first is enough
if self[0].S_0 is not None:
self.S = ArheniusCoeff(self, vm, T, "S_0", "E_S", degree=2)
if self[0].thermal_cond is not None:
self.thermal_cond = ThermalProp(self, vm, T, "thermal_cond", degree=2)
self.heat_capacity = ThermalProp(self, vm, T, "heat_capacity", degree=2)
self.density = ThermalProp(self, vm, T, "rho", degree=2)
if self[0].Q is not None:
self.Q = ThermalProp(self, vm, T, "Q", degree=2)
[docs]
def solubility_as_function(self, mesh, T):
"""
Makes solubility as a fenics.Function and stores it in S attribute
"""
V = f.FunctionSpace(mesh.mesh, "DG", 1)
S = f.Function(V, name="S")
vS = f.TestFunction(V)
dx = mesh.dx
F = 0
for mat in self:
F += -S * vS * dx(mat.id)
F += mat.S_0 * f.exp(-mat.E_S / k_B / T) * vS * dx(mat.id)
f.solve(F == 0, S, bcs=[])
self.S = S
[docs]
def create_solubility_law_markers(self, mesh: festim.Mesh):
"""Creates the attributes henry_marker and sievert_marker
These fenics.Function are equal to one or zero depending
on the material solubility_law
Args:
mesh (festim.Mesh): the mesh
"""
V = f.FunctionSpace(mesh.mesh, "DG", 0)
henry = f.Function(V)
sievert = f.Function(V)
test_function_henry = f.TestFunction(V)
test_function_sievert = f.TestFunction(V)
# initialise formulations
F_henry = -henry * test_function_henry * mesh.dx
F_sievert = -sievert * test_function_sievert * mesh.dx
# build the formulation depending on the
for mat in self:
# make sure mat_ids is a list
mat_ids = mat.id
if not isinstance(mat.id, list):
mat_ids = [mat.id]
for mat_id in mat_ids: # iterate through the subdomains
if mat.solubility_law == "henry":
F_henry += 1 * test_function_henry * mesh.dx(mat_id)
elif mat.solubility_law == "sievert":
F_sievert += 1 * test_function_sievert * mesh.dx(mat_id)
# solve the problems
f.solve(F_henry == 0, henry, [])
f.solve(F_sievert == 0, sievert, [])
self.henry_marker = henry
self.sievert_marker = sievert
class ArheniusCoeff(f.UserExpression):
def __init__(self, materials, vm, T, pre_exp, E, **kwargs):
super().__init__(kwargs)
self._vm = vm
self._T = T
self._materials = materials
self._pre_exp = pre_exp
self._E = E
def eval_cell(self, value, x, ufc_cell):
cell = f.Cell(self._vm.mesh(), ufc_cell.index)
subdomain_id = self._vm[cell]
material = self._materials.find_material_from_id(subdomain_id)
D_0 = getattr(material, self._pre_exp)
E_D = getattr(material, self._E)
value[0] = D_0 * f.exp(-E_D / k_B / self._T(x))
def value_shape(self):
return ()
class ThermalProp(f.UserExpression):
def __init__(self, materials, vm, T, key, **kwargs):
super().__init__(kwargs)
self._T = T
self._vm = vm
self._materials = materials
self._key = key
def eval_cell(self, value, x, ufc_cell):
cell = f.Cell(self._vm.mesh(), ufc_cell.index)
subdomain_id = self._vm[cell]
material = self._materials.find_material_from_id(subdomain_id)
attribute = getattr(material, self._key)
if callable(attribute):
value[0] = attribute(self._T(x))
else:
value[0] = attribute
def value_shape(self):
return ()