# Copyright (C) 2022 Jørgen Schartum Dokken
#
# This file is part of Oasisx
# SPDX-License-Identifier: MIT
from enum import Enum
from typing import Callable, List, Optional, Tuple, Union
from petsc4py import PETSc as _PETSc
import dolfinx.fem as _fem
import dolfinx.mesh as _dmesh
import numpy as np
import numpy.typing as npt
import ufl
from dolfinx import default_scalar_type
__all__ = ["DirichletBC", "PressureBC", "LocatorMethod"]
[docs]class LocatorMethod(Enum):
"""
Search methods for Dirichlet BCs
"""
GEOMETRICAL = 1
TOPOLOGICAL = 2
LocatorMethod.TOPOLOGICAL.__doc__ = "Topogical search for dofs"
LocatorMethod.GEOMETRICAL.__doc__ = "Geometrical search for dofs"
[docs]class DirichletBC:
"""
Create a Dirichlet boundary condition based on topological or geometrical info from the mesh
This boundary condtion should only be used for velocity function spaces.
Args:
value: The value the degrees of freedom should have. It can be a float, a
`dolfinx.fem.Constant` or a lambda-function.
method: Locator method for marker.
marker: If :py:obj:`oasisx.LocatorMethod.TOPOLOGICAL` the input should
be a tuple of a mesh tag and the corresponding value for the entities to assign
Dirichlet conditions to. If :py:obj:`oasisx.LocatorMethod.GEOMETRICAL` the input
is a lambda function taking in x, y, and z coordinates (ordered as
`[[x0, x1,...,xn], [y0,...,yn], [z0,...,zn]]`), and return a boolean marker where
the ith entry indicates if `[xi, yi, zi]` should have a diriclet bc.
Example:
**Assigning a topological condition**
.. highlight:: python
.. code-block:: python
entities = np.array([0,3,8],dtype=np.int32)
values = np.full_like(entities, 2, dtype=np.int32)
mt = dolfinx.fem.meshtags(mesh, mesh.topology.dim-1, entities, values)
bc = DirichletBC(5., LocatorMethod.TOPOLOGICAL, (mt, 2))
**Assigning a geometrical condition**
.. highlight:: python
.. code-block:: python
bc = DirchletBC(dolfinx.fem.Constant(mesh, 3.), lambda x: np.isclose(x[0]))
"""
_method: LocatorMethod
_entities: npt.NDArray[np.int32] # List of entities local to process
_e_dim: int # Dimension of entities
_locator: Callable[[npt.NDArray[np.float64]], npt.NDArray[np.bool_]]
_dofs: npt.NDArray[np.int32]
_value: Union[
np.float64,
_fem.Constant,
Callable[[npt.NDArray[np.float64]], npt.NDArray[np.float64]],
]
_bc: _fem.DirichletBC
_u: Optional[_fem.Function]
__slots__ = tuple(__annotations__)
def __init__(
self,
value: Union[
np.float64,
_fem.Constant,
Callable[[npt.NDArray[np.float64]], npt.NDArray[np.float64]],
],
method: LocatorMethod,
marker: Union[
Tuple[_dmesh.MeshTags, np.int32],
Callable[[npt.NDArray[np.float64]], npt.NDArray[np.bool_]],
],
):
if method == LocatorMethod.GEOMETRICAL:
self._method = method
setattr(self, "_locator", marker)
elif method == LocatorMethod.TOPOLOGICAL:
self._method = method
self._entities = marker[0].find(marker[1]) # type: ignore
self._e_dim = marker[0].dim # type:ignore
self._value = value
def set_dofs(self, dofs: npt.NDArray[np.int32]):
self._dofs = dofs
def _locate_dofs(self, V: _fem.FunctionSpace):
"""
Locate all dofs satisfying the criterion
"""
if self._method == LocatorMethod.GEOMETRICAL:
self._dofs = _fem.locate_dofs_geometrical(V, self._locator) # type:ignore
elif self._method == LocatorMethod.TOPOLOGICAL:
V.mesh.topology.create_connectivity(self._e_dim, V.mesh.topology.dim)
self._dofs = _fem.locate_dofs_topological(V, self._e_dim, self._entities)
def create_bc(self, V: _fem.FunctionSpace):
""" """
if not hasattr(self, "_dofs"):
self._locate_dofs(V)
try:
self._bc = _fem.dirichletbc(self._value, self._dofs, V) # type:ignore
except AttributeError:
self._u = _fem.Function(V)
self._u.interpolate(self._value) # type:ignore
self._bc = _fem.dirichletbc(self._u, self._dofs)
[docs] def update_bc(self):
"""
Update the underlying function if input value is a lambda function
"""
if hasattr(self, "_u"):
self._u.interpolate(self._value)
[docs] def apply(self, x: _PETSc.Vec): # type: ignore
"""
Apply boundary condition to a PETSc vector
"""
_fem.petsc.set_bc(x, [self._bc])
[docs]class PressureBC:
"""
Create a Pressure boundary condition (natural bc) based on a set of facets of a
mesh and some value.
Args:
value: The value the degrees of freedom should have. It can be a float, a
`dolfinx.fem.Constant` or a lambda-function. If `value` is a lambda-function it
is interpolated into the pressure space.
marker: Tuple of a mesh tag and the corresponding value for the entities to assign
Dirichlet conditions to. The meshtag dimension has to be `mesh.topology.dim -1`.
Example:
**Assigning a constant condition**
.. highlight:: python
.. code-block:: python
entities = np.array([0,3,8],dtype=np.int32)
values = np.full_like(entities, 2, dtype=np.int32)
mt = dolfinx.fem.meshtags(mesh, mesh.topology.dim-1, entities, values)
bc = DirichletBC(5., (mt, 2))
**Assigning a time-dependent condition**
.. highlight:: python
.. code-block:: python
class OutletPressure():
def __init__(self, t:float):
self.t = t
def eval(self, x: numpy.typing.NDArray[np.float64]):
return self.t*x[0]
p = OutletPressure(0.)
entities = np.array([0,3,8],dtype=np.int32)
values = np.full_like(entities, 2, dtype=np.int32)
mt = dolfinx.fem.meshtags(mesh, mesh.topology.dim-1, entities, values)
bc = PressureBC(p, (mt, 2))
Q = dolfinx.fem.functionspace(mesh, ("Lagrange", 1))
# Create appropriate structures for assigning bcs
bc.create_bc(Q)
# Update time in bc
p.t = 1
"""
_subdomain_data: _dmesh.MeshTags
_subdomain_id: Union[np.int32, Tuple[np.int32]]
_value: Union[
np.float64,
_fem.Constant,
Callable[[npt.NDArray[np.float64]], npt.NDArray[np.float64]],
]
_u: _fem.Function
_rhs: List[ufl.form.Form]
_bc: _fem.DirichletBC
__slots__ = tuple(__annotations__)
def __init__(
self,
value: Union[
np.float64,
_fem.Constant,
Callable[[npt.NDArray[np.float64]], npt.NDArray[np.float64]],
],
marker: Tuple[_dmesh.MeshTags, Union[np.int32, Tuple[np.int32]]],
):
self._subdomain_data, self._subdomain_id = marker
self._value = value
[docs] def create_bcs(self, V: _fem.FunctionSpace, Q: _fem.FunctionSpace):
"""
Create boundary conditions for the pressure conditions for a given velocity and
pressure function space
Args:
V: The velocity function space
Q: The pressure function space
"""
mesh = V.mesh
assert mesh.topology == self._subdomain_data.topology
# Create pressure "Neumann" condition
v = ufl.TestFunction(V)
ds = ufl.Measure(
"ds",
domain=mesh,
subdomain_data=self._subdomain_data,
subdomain_id=self._subdomain_id,
)
n = ufl.FacetNormal(mesh)
try:
rhs = [self._value * n_i * v.dx(i) * ds for i, n_i in enumerate(n)]
except TypeError:
# If input is lambda function interpolate into local function
self._u = _fem.Function(Q)
self._u.interpolate(self._value) # type: ignore
rhs = [self._u * n_i * v.dx(i) * ds for i, n_i in enumerate(n)]
# Create rhs contribution from natural boundary condition
self._rhs = rhs
# Create homogenuous boundary condition for pressure correction eq
boundary_facets = self._subdomain_data.find(self._subdomain_id) # type: ignore
mesh.topology.create_connectivity(mesh.topology.dim - 1, mesh.topology.dim)
dofs = _fem.locate_dofs_topological(Q, mesh.topology.dim - 1, boundary_facets)
self._bc = _fem.dirichletbc(default_scalar_type(0.0), dofs, Q)
[docs] def update_bc(self):
"""
Update boundary condition if input-value is a lambda function
"""
if hasattr(self, "_u"):
self._u.interpolate(self._value)
@property
def bc(self) -> _fem.DirichletBC:
return self._bc
def rhs(self, i: int) -> _fem.Form:
assert i < len(self._rhs)
return self._rhs[i]