diff --git a/src/bvpy/boundary_conditions/boundary.py b/src/bvpy/boundary_conditions/boundary.py index 395cf123c1ebd89d6f281987ef1ebdd02891579f..8a8979a35c2e0b4f4a93cfb664cecc6f356cb7f6 100644 --- a/src/bvpy/boundary_conditions/boundary.py +++ b/src/bvpy/boundary_conditions/boundary.py @@ -9,6 +9,7 @@ # File contributor(s): # Florian Gacon <florian.gacon@inria.fr> # Olivier Ali <olivier.ali@inria.fr> +# Gonzalo Revilla <gonzalo.revilla-mut@inria.fr> # # File maintainer(s): # Olivier Ali <olivier.ali@inria.fr> @@ -19,177 +20,94 @@ # https://www.gnu.org/licenses/lgpl-3.0.en.html # # ----------------------------------------------------------------------- - +import numpy as np import fenics as fe -from sympy import And, Function, Or, Symbol, Not -from sympy.parsing.sympy_parser import parse_expr - -from bvpy import MPI_COMM_WORLD, logger - -_XYZ = {'x': Symbol('x[0]'), - 'y': Symbol('x[1]'), - 'z': Symbol('x[2]'), - 'all': Symbol('on_boundary'), - 'near': Function('near')} +from bvpy import MPI_COMM_WORLD -class Boundary(object): - """Defines the boundary of a domain. - - Parameters - ---------- - expr : str - An expression used to compute the boundary. +class Boundary: + """Allows to identify subdomains of the boundary given a parametric expression. Attributes ---------- - _string : str - The sting passed to the class - _expr : str - The parsed string for fenics. - _domain : SubDomain - The subdomain fenics defining the boundary. - _kwargs : dict - Optional argument. - - Examples - -------- - >>> Boundary("near(x+a, 2)", a=0) & Boundary("all") + string : str + Parametric expression that defines the boundary subdomain. + kwargs : dict + Parameters (if any) of the parametric expression. + subdomain : fe.CompiledSubDomain + Fenics object that represents the current subdomain. + + Methods + ------- + mark_mesh_function() + Given a fenics mesh function, mark the elements of the current boundary subdomain. """ def __init__(self, expr, **kwargs): - """Constructor of the Boundary class. - + """ Parameters ---------- expr : str - An expression used to compute the boundary. + An expression used to compute the subdomain. + **kwargs + Expression parameters that will be sent to the fenics object. (See next example) - Yields - ------- - Boundary - A boundary instance. + Examples + -------- + >>> b1 = Boundary("near(x, 0)") & Boundary("near(x+a, 2)", a=0) + >>> b2 = Boundary("all") + >>> b3 = Boundary("near(x, 0)") | Boundary("near(x, 1)") + >>> b4 = ~Boundary("near(x, 1)") """ - self._string = expr - self._expr = parse_expr(expr, local_dict=_XYZ, evaluate=False) - self._domain = fe.CompiledSubDomain(str(self._expr), **kwargs, - mpi_comm=MPI_COMM_WORLD) - self._kwargs = kwargs + self.__string = expr + replacements = {'x': 'x[0]', 'y': 'x[1]', 'z': 'x[2]', 'all': 'on_boundary', '~': '!'} + for old, new in replacements.items(): + expr = expr.replace(old, new) + self.__kwargs = kwargs - def __call__(self): - """Fetches the fenics object defining the boundary. + self.__subdomain = fe.CompiledSubDomain(expr, **kwargs, mpi_comm=MPI_COMM_WORLD) - Returns - ------- - Domain - Description of returned object. + @property + def string(self): + return self.__string - """ - return self._domain - - def __or__(self, other): - """Intersects two boundary domains together. - - Parameters - ---------- - other : type - Description of parameter `other`. + @property + def kwargs(self): + return self.__kwargs - Returns - ------- - type - Description of returned object. + @property + def subdomain(self): + return self.__subdomain + def __or__(self, other): + """Creates a boundary instance: The addition of the 2 given boundaries objects. """ - new = Boundary('near(x, 0)') - s1 = Symbol(str(self._expr)) - s2 = Symbol(str(other._expr)) - new._expr = Or(s1, s2) - new._string = self._string+' | '+other._string - new._kwargs = {**self._kwargs, **other._kwargs} - str_expr = str(new._expr).replace('&', '&&')\ - .replace('|', '||')\ - .replace('~', '!') - new._domain = fe.CompiledSubDomain(str_expr, **new._kwargs, - mpi_comm=MPI_COMM_WORLD) - return new + or_string = '(' + self.string+' || '+other.string+')' + return Boundary(or_string, **{**self.kwargs, **other.kwargs}) def __and__(self, other): - """Adds two boundary domains together. - - Parameters - ---------- - other : type - Description of parameter `other`. - - Returns - ------- - type - Description of returned object. - + """Creates a boundary instance: The intersection of the 2 given boundaries objects. """ - new = Boundary('near(x, 0)') - s1 = Symbol(str(self._expr)) - s2 = Symbol(str(other._expr)) - new._expr = And(s1, s2) - new._string = self._string+' & '+other._string - new._kwargs = {**self._kwargs, **other._kwargs} - str_expr = str(new._expr).replace('&', '&&')\ - .replace('|', '||')\ - .replace('~', '!') - logger.debug('expression in and: %s ', str_expr) - new._domain = fe.CompiledSubDomain(str_expr, **new._kwargs, - mpi_comm=MPI_COMM_WORLD) - - return new + and_string = '(' + self.string+' && '+other.string+')' + return Boundary(and_string, **{**self.kwargs, **other.kwargs}) def __invert__(self): - """Inverts a boundary domain. - - Parameters - ---------- - other : type - Description of parameter `other`. - Returns - ------- - type - Description of returned object. + """Creates a boundary instance: the inverse of the current one. """ - new = Boundary('near(x, 0)') - s1 = Symbol(str(self._expr)) - new._expr = Not(s1) - new._string = '~ ('+self._string+')' - new._kwargs = self._kwargs - str_expr = str(new._expr).replace('&', '&&')\ - .replace('|', '||')\ - .replace('~', '!') - logger.debug('expression in and: %s ', str_expr) - new._domain = fe.CompiledSubDomain(str_expr, **new._kwargs, - mpi_comm=MPI_COMM_WORLD) - - return new - - def markMeshFunction(self, valueTrue, - mesh=None, valueFalse=0, meshfunc=None, - dim_topology=0, type='double'): - """Short summary. + invert_string = '~('+self.string+')' + return Boundary(invert_string, **self.kwargs) + + def mark_mesh_function(self, mf :fe.MeshFunction, value_true: int=1): + """Given a fenics mesh function, mark the elements of the current boundary subdomain. Parameters ---------- - valueTrue : type - Description of parameter `valueTrue`. - mesh : type - Description of parameter `mesh` (the default is None). - valueFalse : type - Description of parameter `valueFalse` (the default is 0). - meshfunc : type - Description of parameter `meshfunc` (the default is None). - dim_topology : type - Description of parameter `dim_topology` (the default is 0). - type : type - Description of parameter `type` (the default is 'double'). + mf : fe.Function + The fenics mesh function that will be marked. + value_true : int + Label that will have the elements of the current boundary subdomain. Returns ------- @@ -197,11 +115,12 @@ class Boundary(object): Description of returned object. """ - if meshfunc is not None: - self._domain.mark(meshfunc, valueTrue) # this mark the subdomain meshfunc using the condition - else: - meshfunc = fe.MeshFunction(type, mesh, dim_topology) - meshfunc.set_all(valueFalse) - self._domain.mark(meshfunc, valueTrue) - - return meshfunc + if np.any(mf.array() == value_true): + raise ValueError(f'The mesh function already contains some elements with the label {value_true}') + + self.subdomain.mark(mf, value_true) + + if np.all(mf.array() != value_true): + raise ValueError(f'The mesh function has not marked any vertex') + + # TODO : assert that all the marked vertex are on the boundary. Maybe use fe.BoundaryMesh(mesh, "exterior") diff --git a/src/bvpy/boundary_conditions/dirichlet.py b/src/bvpy/boundary_conditions/dirichlet.py index b1d018d0e78e1831a70f32c5e422f7b85451d45a..1220fa39b7c6eb22dad36b3580cd969f3cc813e6 100644 --- a/src/bvpy/boundary_conditions/dirichlet.py +++ b/src/bvpy/boundary_conditions/dirichlet.py @@ -86,16 +86,16 @@ class ConstantDirichlet(object): """ if self._subspace is None: dir = fe.DirichletBC(functionSpace, - fe.Constant(self._val), self._boundary(), + fe.Constant(self._val), self._boundary.subdomain, method=self._method) else: dir = fe.DirichletBC(functionSpace.sub(self._subspace), fe.Constant(self._val), - self._boundary(), method=self._method) + self._boundary.subdomain, method=self._method) if not dir.get_boundary_values(): logger.warning('No nodes are marked for this domain: ' - + str(self._boundary._string)) + + str(self._boundary.string)) return dir @@ -109,7 +109,7 @@ class ConstantDirichlet(object): """ return "<ConstantDirichlet object, location: "\ - + str(self._boundary._string) + ", value: " + str(self._val)+">" + + str(self._boundary.string) + ", value: " + str(self._val)+">" class VariableDirichlet(object): @@ -193,17 +193,17 @@ class VariableDirichlet(object): if self._subspace is None: dir = fe.DirichletBC(functionSpace, expr, - self._boundary(), + self._boundary.subdomain, method=self._method) else: dir = fe.DirichletBC(functionSpace.sub(self._subspace), expr, - self._boundary(), + self._boundary.subdomain, method=self._method) if not dir.get_boundary_values(): logger.warning('No nodes are marked for this domain: ' - + str(self._boundary._string)) + + str(self._boundary.string)) return dir @@ -217,7 +217,7 @@ class VariableDirichlet(object): """ return "<VariableDirichlet object, location: "\ - + str(self._boundary._string) + ", value: "\ + + str(self._boundary.string) + ", value: "\ + str(self._val) + ">" @@ -345,11 +345,11 @@ class NormalDirichlet(object): """ return "<NormalDirichlet object, location: "\ - + str(self._boundary._string) + ", value: " + str(self._val)+">" + + str(self._boundary.string) + ", value: " + str(self._val)+">" def apply(self, functionSpace): return fe.DirichletBC(functionSpace, boundary_normal(functionSpace.mesh(), scale=self._val), - self._boundary(), + self._boundary.subdomain, method=self._method) diff --git a/src/bvpy/boundary_conditions/neumann.py b/src/bvpy/boundary_conditions/neumann.py index a6b4c3c7e9e887e0f05161ee007baeb3addd45a5..db07aaaa52e89c68c0303132ada0c207d6381ed2 100644 --- a/src/bvpy/boundary_conditions/neumann.py +++ b/src/bvpy/boundary_conditions/neumann.py @@ -69,11 +69,11 @@ class NeumannInterface(object): raise NotImplementedError def markMeshFunction(self, mf): - self._boundary.markMeshFunction(self._id, meshfunc=mf) + self._boundary.mark_mesh_function(mf, self._id) def __repr__(self): return "<" + self.__class__.__name__ + " object, location: " +\ - str(self._boundary._string) + ", value: " + str(self._val) + ">" + str(self._boundary.string) + ", value: " + str(self._val) + ">" class ConstantNeumann(NeumannInterface): @@ -186,7 +186,7 @@ class NormalNeumann(NeumannInterface): """ return "<NormalNeumann object, location: "\ - + str(self._boundary._string) + ", value: " + str(self._val)+">" + + str(self._boundary.string) + ", value: " + str(self._val)+">" def apply(self, functionSpace, mf=None): if mf is not None: diff --git a/test/test_boundary.py b/test/test_boundary.py index 5725177e9fdda963ce025404c923124c7415e1a1..361517202f5773558c2b16b74389f73f98cc7784 100644 --- a/test/test_boundary.py +++ b/test/test_boundary.py @@ -1,4 +1,5 @@ import unittest +import numpy as np from bvpy.boundary_conditions.boundary import Boundary import fenics as fe @@ -7,37 +8,67 @@ class TestBoundary(unittest.TestCase): def setUp(self): """Initiates the test. """ + self.mesh = fe.UnitSquareMesh(2, 2) + self.mf = fe.MeshFunction("size_t", self.mesh, 0) # 0 means defined on the mesh vertices def tearDown(self): """Concludes and closes the test. """ + def assert_mf(self, computed, expected): + self.assertTrue(np.array_equal(computed, expected)) + + def test_near(self): + b1 = Boundary("near(x, 0)") + b1.mark_mesh_function(self.mf, 1) + self.assert_mf(self.mf.array(), np.array([1, 0, 0, 1, 0, 0, 1, 0, 0])) + + def test_all(self): + b1 = Boundary("all") + b1.mark_mesh_function(self.mf, 1) + self.assert_mf(self.mf.array(), np.array([1, 1, 1, 1, 0, 1, 1, 1, 1])) + + def test_kwargs(self): + b1 = Boundary("near(x, a)", a=1) + b1.mark_mesh_function(self.mf, 1) + self.assert_mf(self.mf.array(), np.array([0, 0, 1, 0, 0, 1, 0, 0, 1])) + def test_and(self): b1 = Boundary("near(x, 0)") b2 = Boundary("near(y, 0)") + b3 = b1 & b2 - self.assertEqual(b3._string, "near(x, 0) & near(y, 0)") + b3.mark_mesh_function(self.mf, 1) + self.assert_mf(self.mf.array(), np.array([1, 0, 0, 0, 0, 0, 0, 0, 0])) def test_or(self): b1 = Boundary("near(x, 0)") b2 = Boundary("near(y, 0)") + b3 = b1 | b2 - self.assertEqual(b3._string, "near(x, 0) | near(y, 0)") + b3.mark_mesh_function(self.mf, 1) + self.assert_mf(self.mf.array(), np.array([1, 1, 1, 1, 0, 0, 1, 0, 0])) def test_invert(self): b1 = Boundary("near(x, 0)") b2 = ~ b1 - self.assertEqual(b2._string, "~ (near(x, 0))") + b2.mark_mesh_function(self.mf, 1) + self.assert_mf(self.mf.array(), np.array([0, 1, 1, 0, 1, 1, 0, 1, 1])) - def test_mark(self): + def test_and_or(self): b1 = Boundary("near(x, 0)") - mesh = fe.UnitSquareMesh(2, 2) - mf = fe.MeshFunction("size_t", mesh, 0, 0) - b1.markMeshFunction(1, meshfunc=mf) - self.assertEqual(mf[0], 1) - self.assertEqual(mf[3], 1) - self.assertEqual(mf[6], 1) - mf2 = b1.markMeshFunction(1, type="size_t", mesh=mesh) - self.assertEqual(mf2[0], 1) - self.assertEqual(mf2[3], 1) - self.assertEqual(mf2[6], 1) + b2 = Boundary("near(y, 0)") + b3 = Boundary("near(x, 1)") + + b4 = (b1 & ~b2) | b3 + b4.mark_mesh_function(self.mf, 1) + self.assert_mf(self.mf.array(), np.array([0, 0, 1, 1, 0, 1, 1, 0, 1])) + + def test_or_and(self): + b1 = Boundary("near(x, 0)") + b2 = Boundary("near(y, 0)") + b3 = Boundary("near(x, 1)") + + b4 = b1 & (b2 | b3) + b4.mark_mesh_function(self.mf, 1) + self.assert_mf(self.mf.array(), np.array([1, 0, 0, 0, 0, 0, 0, 0, 0]))