import types
from collections.abc import Callable
import dolfinx
import numpy as np
from dolfinx import fem
from dolfinx.mesh import Mesh, locate_entities
from numpy import typing as npt
from scifem.mesh import transfer_meshtags_to_submesh
from festim.material import Material
from festim.subdomain.surface_subdomain import SurfaceSubdomain
# Define the appropriate method based on the version
try:
from dolfinx.mesh import EntityMap
entity_map_type = EntityMap
except ImportError:
entity_map_type = npt.NDArray[np.int32]
[docs]
class VolumeSubdomain:
"""Volume subdomain class.
Args:
id: the id of the volume subdomain (> 0)
submesh: the submesh of the volume subdomain
cell_map: the cell map of the volume subdomain
parent_mesh: the parent mesh of the volume subdomain
parent_to_submesh: the parent to submesh map of the volume subdomain
v_map: the vertex map of the volume subdomain
n_map: the normal map of the volume subdomain
facet_to_parent: the facet to parent map of the volume subdomain
ft: the facet meshtags of the volume subdomain
padded: whether the subdomain is padded (for 0.9 compatibility)
u: the solution function of the subdomain
u_n: the previous solution function of the subdomain
material: the material assigned to the subdomain
sub_T: the sub temperature field in the subdomain
"""
id: int
submesh: dolfinx.mesh.Mesh
cell_map: "entity_map_type"
parent_mesh: dolfinx.mesh.Mesh
parent_to_submesh: "entity_map_type"
v_map: "entity_map_type"
n_map: np.ndarray
facet_to_parent: np.ndarray
ft: dolfinx.mesh.MeshTags
padded: bool # NOTE: Once 0.9 support is dropped, this can be removed
u: dolfinx.fem.Function
u_n: dolfinx.fem.Function
material: Material
sub_T: fem.Function | float
def __init__(
self, id, material, locator: Callable | None = None, name: str | None = None
):
assert id != 0, "Volume subdomain id cannot be 0"
self.id = id
self.material = material
self.locator = locator
self.name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if value is None:
self._name = None
elif isinstance(value, str):
self._name = value
else:
raise TypeError("Name must be a string")
[docs]
def create_subdomain(self, mesh: dolfinx.mesh.Mesh, marker: dolfinx.mesh.MeshTags):
"""
Creates the following attributes: ``.parent_mesh``, ``.submesh``, ``.cell_map``,
``.v_map``, ``padded``, and the entity map ``parent_to_submesh``.
Only used in ``festim.HydrogenTransportProblemDiscontinuous``
Args:
mesh (dolfinx.mesh.Mesh): the parent mesh
marker (dolfinx.mesh.MeshTags): the parent volume markers
"""
assert marker.dim == mesh.topology.dim
self.parent_mesh = (
mesh # NOTE: it doesn't seem like we use this attribute anywhere
)
entities = marker.find(self.id)
self.submesh, self.cell_map, self.v_map, self.n_map = (
dolfinx.mesh.create_submesh(mesh, marker.dim, entities)
)
if isinstance(entity_map_type, types.GenericAlias):
num_cells_local = (
mesh.topology.index_map(marker.dim).size_local
+ mesh.topology.index_map(marker.dim).num_ghosts
)
self.parent_to_submesh = np.full(num_cells_local, -1, dtype=np.int32)
self.parent_to_submesh[self.cell_map] = np.arange(
len(self.cell_map), dtype=np.int32
)
self.padded = False
def transfer_meshtag(self, mesh: dolfinx.mesh.Mesh, tag: dolfinx.mesh.MeshTags):
# Transfer meshtags to submesh
assert self.submesh is not None, "Need to call create_subdomain first"
self.ft, self.facet_to_parent = transfer_meshtags_to_submesh(
tag, self.submesh, self.v_map, self.cell_map
)
[docs]
def locate_subdomain_entities(self, mesh: Mesh) -> npt.NDArray[np.int32]:
"""Locates all cells in subdomain borders within domain.
Args:
mesh: the mesh of the model
Returns:
entities: the entities of the subdomain
"""
if self.locator is None:
raise ValueError("No locator function provided for locating cells.")
entities = locate_entities(mesh, mesh.topology.dim, self.locator)
return entities
[docs]
class VolumeSubdomain1D(VolumeSubdomain):
"""Volume subdomain class for 1D cases.
Args:
id (int): the id of the volume subdomain
borders (list of float): the borders of the volume subdomain
material (festim.Material): the material of the volume subdomain
Attributes:
id (int): the id of the volume subdomain
borders (list of float): the borders of the volume subdomain
material (festim.Material): the material of the volume subdomain
Examples:
.. testsetup:: VolumeSubdomain1D
from festim import VolumeSubdomain1D, Material
my_mat = Material(D_0=1, E_D=1, name="test_mat")
.. testcode:: VolumeSubdomain1D
VolumeSubdomain1D(id=1, borders=[0, 1], material=my_mat)
"""
def __init__(self, id, borders, material) -> None:
super().__init__(
id,
material,
locator=lambda x: np.logical_and(x[0] >= borders[0], x[0] <= borders[1]),
)
self.borders = borders
def find_volume_from_id(id: int, volumes: list):
"""Returns the correct volume subdomain object from a list of volume ids based on an
int.
Args:
id (int): the id of the volume subdomain
volumes (list): the list of volumes
Returns:
festim.VolumeSubdomain: the volume subdomain object with the correct id
Raises:
ValueError: if the volume name is not found in the list of volumes
"""
for vol in volumes:
if vol.id == id:
return vol
raise ValueError(f"id {id} not found in list of volumes")
[docs]
def map_surface_to_volume_subdomains(
ft: dolfinx.mesh.MeshTags,
ct: dolfinx.mesh.MeshTags,
facet_to_cell: dolfinx.cpp.graph.AdjacencyList_int32,
volume_subdomains: list[VolumeSubdomain],
surface_subdomains: list[SurfaceSubdomain],
comm=None,
) -> dict[SurfaceSubdomain, VolumeSubdomain]:
"""Maps surface subdomains to volume subdomains based on the facet and cell meshtags
and the facet to cell connectivity.
Raises:
AssertionError: if a surface subdomain is connected to multiple volume
subdomains
Args:
ft: the facet meshtags of the parent mesh
ct: the cell meshtags of the parent mesh
facet_to_cell: the facet to cell connectivity of the parent mesh
volume_subdomains: the list of volume subdomains
surface_subdomains: the list of surface subdomains
comm: MPI communicator (required for parallel runs)
Returns:
dict[SurfaceSubdomain, VolumeSubdomain]: a dictionary mapping surface subdomains
to volume subdomains
"""
# get connected cells for tagged facets
start_indices = facet_to_cell.offsets[ft.indices]
end_indices = facet_to_cell.offsets[ft.indices + 1]
num_connections = end_indices - start_indices
# A facet is connected to at most 2 cells (boundary = 1, interior = 2)
cell_ids_0 = facet_to_cell.array[start_indices]
has_second_cell = num_connections == 2
cell_ids_1 = facet_to_cell.array[start_indices[has_second_cell] + 1]
connected_cells = np.concatenate([cell_ids_0, cell_ids_1])
connected_facet_tags = np.concatenate([ft.values, ft.values[has_second_cell]])
# map connected cells to their cell tags
sort_idx = np.argsort(ct.indices)
sorted_ct_indices = ct.indices[sort_idx]
sorted_ct_values = ct.values[sort_idx]
idx = np.searchsorted(sorted_ct_indices, connected_cells)
# mask out-of-bounds
valid = idx < len(sorted_ct_indices)
# of those in bounds, check if they actually match
valid[valid] = sorted_ct_indices[idx[valid]] == connected_cells[valid]
valid_cell_tags = sorted_ct_values[idx[valid]]
valid_facet_tags = connected_facet_tags[valid]
unique_pairs = np.unique(np.vstack((valid_facet_tags, valid_cell_tags)).T, axis=0)
if comm is not None and comm.size > 1:
all_pairs = comm.allgather(unique_pairs)
non_empty = [p for p in all_pairs if len(p) > 0]
if non_empty:
unique_pairs = np.unique(np.vstack(non_empty), axis=0)
surface_tag_to_subdomain = {s.id: s for s in surface_subdomains}
volume_tag_to_subdomain = {v.id: v for v in volume_subdomains}
surface_to_subdomain = {}
for s_tag, v_tag in unique_pairs:
dolfinx.log.log(
dolfinx.log.LogLevel.INFO,
f"Facet tag {s_tag} is connected to cell tag {v_tag}",
)
s_subdomain = surface_tag_to_subdomain.get(s_tag)
v_subdomain = volume_tag_to_subdomain.get(v_tag)
if s_subdomain and v_subdomain:
if s_subdomain in surface_to_subdomain:
assert surface_to_subdomain[s_subdomain] == v_subdomain, (
f"Surface subdomain {s_subdomain.id} is connected "
f"to multiple volume subdomains: "
f"{surface_to_subdomain[s_subdomain].id} and {v_subdomain.id}"
)
else:
surface_to_subdomain[s_subdomain] = v_subdomain
return surface_to_subdomain