Best way to mark a subdoamain based on interpolated function

Taken we have the following function/solution of a previous simulation:

from mpi4py import MPI
from dolfinx.mesh import CellType, create_unit_square
from dolfinx.fem import functionspace, Function
from dolfinx.io import XDMFFile
from basix.ufl import element
import numpy as np

element_type = "Lagrange"
element_dof = 2

mesh = create_unit_square(
    MPI.COMM_WORLD,  
    points=((0.0, 0.0), (1.0, 1.0)), 
    n=(N, N), 
    CellType.triangle
)
elem = element(element_type, mesh.basix_cell(), element_dof)
V = functionspace(mesh, elem)

psi = Function(V)

# Interpolate some random function
radius = 0.3
def phi(x):  # in my usecase I dont actually know phi, and only have psi
    return np.sqrt((x[0] - 0.5)**2 + (x[1]-0.5)**2) - radius

psi.interpolate(phi)

(addopted form this question: Identifying subdomains from level set function on fenicsx)

How would I mark all subdomains similar to the following pseudo code?

tol = 1e-8
if psi(x) >= tol:
    # Domain 1
else:
    # Domain 2

The post I am referring to utilized:

locate_entities

for this, but this requires to to have phi(x), which I dont have (or do I have to read out the value of psi for every point, which seems less optimal?)

I found the following post for dolfin, which uses some “marker”-magic:
How to define a Function in a submesh based on function values present in an adjoining submesh?
But I cant find a way to adapted to my usecase in dolfinx.
I would be very happy for any help!

This Depends a bit on how you want to quantity psi(x) on a domain. Since a domain is a cell Where psi(x) can be both positive and negative, you need to decide on some criteria.

Suggestion:

  1. psi(midpoint(cell_i))>=0
  2. N/M vertices of a cell satisfies psi(vertices_i)>=tol, i=1,…,M
  3. average value of psi in cell>=tol

Each of these approaches requires slightly different implementations.

I want to utilize higher-order elements later so I think best would be to go with option 2?

High order as in higher order geometry, or higher order function space for psi?

I would then probably go for 3., as it would bias stretching of the domain.

Sorry, that I was not clear. I meant higher order function space.
But would 3 not lead to the boundary between the domains being also the boundary of the cell?
Does this not lead to require a finer mesh size and not utilize the higher order function space?

Well, you said you wanted to have a marker for the mesh that uses something similar to locate_entities. Then you would have to mark whole cells or not.

If you want something more generic, I would use ufl.conditional, as explained in for instance
http://jsdokken.com/FEniCS23-tutorial/src/form_compilation.html#conditional-values
using something like ufl.conditional(ufl.gt(psi, tol), 1,0) which inside a form it will be evaluted at quadrature points.

What I meant, was that I want to define some subdomains based on a the value of a function. And “normaly” this seems to be made by marking the cells. if there is a better way like ufl.conditional I am happy to try it out.

@dokken I implemented ufl.conditional and tested it with the following minimal example:

from mpi4py import MPI
from dolfinx.mesh import CellType, create_unit_square
from dolfinx.fem import functionspace, Function
from dolfinx.io import XDMFFile
from basix.ufl import element
import numpy as np

element_type = "Lagrange"
element_dof = 1  # <-- increasing this should lead in a lower error
N = 90

mesh = create_unit_square(
    MPI.COMM_WORLD,
    nx=N, ny = N,
    cell_type = CellType.triangle
)
elem = element(element_type, mesh.basix_cell(), element_dof)
V = functionspace(mesh, elem)

psi = Function(V)

# Interpolate some random function
radius = 0.3
def phi(x):  # in my usecase I dont actually know phi, and only have psi
    return np.sqrt((x[0] - 0.5)**2 + (x[1]-0.5)**2) - radius

psi.interpolate(phi)


import ufl
import dolfinx.fem as fem

tol = 1e-8
condition = ufl.lt(psi, tol)
true_statement = 1
false_statement = 0
f = ufl.conditional(condition, true_statement, false_statement)
dx = ufl.Measure("dx", domain=mesh)
markers = fem.form(f * dx)
area = fem.assemble_scalar(markers)

area_true = np.pi * radius**2
err_abs = np.abs(area_true-area)
print(err_abs)

What I would expect is that element_dof i.e 5 would result in a smaller error (err_abs). But as a matter effect the err_abs does not change at all, when I change element_dof and seems only be effected by ``N`! Did I miss something? :thinking:

The accuracy of the area is quite close for any degree.
You can further investigate the behavior by varying the quadrature degree as well as the element degree:

import dolfinx.fem as fem
import ufl
from mpi4py import MPI
from dolfinx.mesh import CellType, create_unit_square
from dolfinx.fem import functionspace, Function
from dolfinx.io import VTXWriter
from basix.ufl import element
import numpy as np


def compute_area(N, degree, quadrature_degree):
    metadata = {"quadrature_degree": quadrature_degree}
    mesh = create_unit_square(
        MPI.COMM_WORLD,
        nx=N, ny=N,
        cell_type=CellType.triangle
    )
    element_type = "Lagrange"
    elem = element(element_type, mesh.basix_cell(), degree)
    V = functionspace(mesh, elem)

    psi = Function(V)

    # Interpolate some random function
    radius = 0.3

    def phi(x):  # in my usecase I dont actually know phi, and only have psi
        return np.sqrt((x[0] - 0.5)**2 + (x[1]-0.5)**2) - radius

    psi.interpolate(phi)

    tol = 1e-8
    condition = ufl.lt(psi, tol)
    true_statement = 1
    false_statement = 0
    f = ufl.conditional(condition, true_statement, false_statement)
    dx = ufl.Measure("dx", domain=mesh)
    markers = fem.form(f * dx(metadata=metadata))
    area = fem.assemble_scalar(markers)

    area_true = np.pi * radius**2
    err_abs = np.abs(area_true-area)
    return err_abs


for degree in [1, 2, 3, 4, 5]:
    for quadrature_degree in [1, 2, 4, 8]:
        error = compute_area(
            90, quadrature_degree=quadrature_degree, degree=degree)
        print(degree, quadrature_degree, error)
1 1 0.00021962413986864604
1 2 0.0002196241398873533
1 4 0.00010873517648901654
1 8 5.68679100290681e-05
2 1 0.0007134513003625997
2 2 0.0002196241398873533
2 4 4.000493908035141e-05
2 8 1.439905672551145e-05
3 1 0.0007134513003625997
3 2 0.0002196241398873533
3 4 4.000493908035141e-05
3 8 1.439905672551145e-05
4 1 0.0007134513003625997
4 2 0.0002196241398873533
4 4 4.000493908035141e-05
4 8 1.439905672551145e-05
5 1 0.0007134513003625997
5 2 0.0002196241398873533
5 4 4.000493908035141e-05
5 8 1.439905672551145e-05