Cumulative integral of an interpolated expression

To start, I am on the Dolfinx nightly version which is recognized as version DOLFINx version: 0.10.0.0, and I’m on Python version 3.12.3.

Without getting into any of the specifics of the full problem that I’m working on, I need to evaluate the following cumulative integral

\int_0^x v_h|_{y=0} dx
where v_h pertains to the \hat{\textbf{j}}-component of the vector \textbf{V}_h = u_h \hat{\textbf{i}} + v_h \hat{\textbf{j}}

\textbf{V}_h was evaluated in my code via interpolating a ufl expression. Here is my code for creating \textbf{V}_h

from mpi4py import MPI
import pyvista as pv
import dolfinx as df
import ufl
import gmsh
import numpy as np

Nx,Ny = 60,60 # Number of points in x and y

mesh = df.mesh.create_unit_square(MPI.COMM_WORLD, Nx, Ny)
tdim, fdim = mesh.topology.dim, mesh.topology.dim-1
mesh.topology.create_connectivity(fdim, tdim)

# Define a true functionspace for later usage
S = df.fem.functionspace(mesh, ("Lagrange", 1)) # scalar space for grids and projection of scalar quantities

# Define total wind field
V = df.fem.functionspace(mesh, ("Lagrange", 1, (2,))) # 2D Vector function space
wind = df.fem.Function(V)
wind.interpolate(lambda x: (3*(x[0]-x[1]),3*x[0]+x[1]-2))

# Extract vertical relative vorticity from wind
Z_expr = df.fem.Expression(wind[1].dx(0) - wind[0].dx(1),S.element.interpolation_points)
Z = df.fem.Function(S)
Z.interpolate(Z_expr) # vorticity

# Define boundary condition
boundary_facets = df.mesh.exterior_facet_indices(mesh.topology)
boundary_dofs = df.fem.locate_dofs_topological(S, fdim, boundary_facets)
bc = df.fem.dirichletbc(df.default_scalar_type(0),boundary_dofs,S)

# Solve
u = ufl.TrialFunction(S)
v = ufl.TestFunction(S)
a = ufl.inner(ufl.grad(u),ufl.grad(v)) * ufl.dx
L = -Z*v*ufl.dx
from dolfinx.fem.petsc import LinearProblem
problem = LinearProblem(a=a,L=L,bcs = [bc], petsc_options_prefix="Streamfunction_problem",petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) 
psi_I = problem.solve()

# Extract horizontal divergence from wind field (onto scalar space)
d_expr = df.fem.Expression(ufl.div(wind),S.element.interpolation_points)
d = df.fem.Function(S)
d.interpolate(d_expr) # Evaluate div epression

# Solve
u = ufl.TrialFunction(S)
v = ufl.TestFunction(S)

a = ufl.inner(ufl.grad(u),ufl.grad(v)) * ufl.dx
L = -d*v*ufl.dx

problem = LinearProblem(a=a,L=L,bcs = [bc], petsc_options_prefix="Streamfunction_problem",petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) 
chi_I = problem.solve()

###########################################################################################
# And now we have V_h, and we're ready to begin working on resolving the boundary values ##
###########################################################################################
 
V_h = df.fem.Function(V)  # Define V_h in our 2D vector space, V
V_h_expr = df.fem.Expression((wind - (ufl.grad(chi_I) + ufl.perp(ufl.grad(psi_I)))), V.element.interpolation_points)
V_h.interpolate(V_h_expr) # Evaluate V_h

So it was interpolated onto a 2D functionspace, V, and now I need to cumulatively integrate the v_h component located on the southern boundary of the square i.e. where y=0 and from 0 to x. To do this, I need to extract from V_h all of its v_h component values and find all of them located on the boundary at y=0 from 0\le x\le 1, make sure that these values which I’ve extracted are properly ordered, and then use something like scipy to perform cumulative integration, or maybe use something built into Dolfinx which could do this computation for me. Can this be done?


If the full problem and additional context are necessary, I can provide it, just let me know, but I feel like for what I’m asking, this should be sufficient. In the code, I solved 2 Poisson Equations with homogeneous boundary conditions, and used the 2 solutions to obtain V_h. Now I need to cumulatively integrate a component of V_h.

As far as I see, it can be done in two ways. We can solve a new PDE problem \frac{\partial F}{\partial x} = v_h where F is the unknown anti-derivative (cumulative integral). With appropriate boundary conditions I expect it to be possible to get the desired result. An alternate approach is to define a function that performs the anti-derivative and interpolate a dolfinx function on it. Below is a 1D example that demonstrates the idea:

import dolfinx, ufl, gmsh, petsc4py, mpi4py
from dolfinx.io import gmshio
import numpy as np
import matplotlib.pyplot as plt

gmsh.finalize()
gmsh.initialize()
gmsh.option.setNumber("General.Terminal", 0)
occ = gmsh.model.occ
gdim = 1

p1 = occ.addPoint(0, 0, 0)
p2 = occ.addPoint(2*np.pi, 0, 0)
fov = occ.addLine(p1, p2)
occ.remove_all_duplicates()
occ.synchronize()

all_doms = occ.getEntities(gdim)
for j, dom in enumerate(all_doms):
    gmsh.model.addPhysicalGroup(dom[0], [dom[1]], j + 1)

# number all boundaries
all_edges = gmsh.model.getEntities(gdim-1)
for j, edge in enumerate(all_edges):
    gmsh.model.addPhysicalGroup(edge[0], [edge[1]], edge[1])

gmsh.model.mesh.generate()   
gmsh.model.mesh.refine()
gmsh.model.mesh.refine()

mpi_rank = 0
mesh, ct, ft = gmshio.model_to_mesh(gmsh.model, mpi4py.MPI.COMM_WORLD, mpi_rank, gdim)

mpi_rank = 0

Omega = dolfinx.fem.functionspace(mesh, ("CG", 1))

# known function
fx = dolfinx.fem.Function(Omega)
Fx = dolfinx.fem.Function(Omega)
fx.interpolate(lambda x: np.cos(x[0]))

dx = ufl.Measure("dx", domain=mesh, subdomain_data=ct)
x = ufl.SpatialCoordinate(mesh)[0]

# anti-derivative
Fx_integral = lambda x0: np.hstack([dolfinx.fem.assemble_scalar(dolfinx.fem.form(fx*ufl.conditional(x < x_, 1, 0)*dx)) for x_ in x0[0]])
Fx.interpolate(Fx_integral)

fig, ax = plt.subplots()
x_axis = Omega.tabulate_dof_coordinates()[:,0]
sort_idx = np.argsort(x_axis)
ax.plot(x_axis[sort_idx], fx.x.array[sort_idx], label="$f(x)$")
ax.plot(x_axis[sort_idx], Fx.x.array[sort_idx], label="$F(x)$")
ax.set_xlabel('$x$')
ax.legend(frameon=False)
ax.grid()

One could make some modifications to:

I.e. make the restriction operator spatially dependent (and 2D-1D, rather than 3D-1D).

It would require some tinkering though.

Running your sample for latter method worked just fine, but there were a couple errors which could be due to us being on different versions of gmsh and dolfinx. I checked to see if Dolfinx didn’t have a built-in function which could create a mesh as a line and found dolfinx.mesh.create_interval() so that gets around the gmsh issue. Here was the slightly updated code

from mpi4py import MPI
import dolfinx as df
import ufl
import numpy as np

Nx = 10 # Number of x-points in mesh
# 1-D Mesh
line = df.mesh.create_interval(MPI.COMM_WORLD,Nx,np.array([0,np.pi]))

Omega = df.fem.functionspace(line, ("CG", 1)) # functionspace

fx = df.fem.Function(Omega)
Fx = df.fem.Function(Omega)
fx.interpolate(lambda x: np.cos(x[0]))

x = ufl.SpatialCoordinate(line)[0]
Fx_integral = lambda x0: np.hstack([df.fem.assemble_scalar(df.fem.form(fx*ufl.conditional(x < x_, 1, 0)*ufl.dx)) for x_ in x0[0]])
Fx.interpolate(Fx_integral)

This gave a highly accurate result.

Attempts to setup a variational problem kept failing on my end, however

def left_boundary(x):
    return np.isclose(x[0],0)
def right_boundary(x):
    return np.isclose(x[0],np.pi)
left_dof = df.fem.locate_dofs_geometrical(Omega,left_boundary)
right_dof = df.fem.locate_dofs_geometrical(Omega,right_boundary)

bc_left = df.fem.dirichletbc(df.default_scalar_type(0),left_dof,Omega)
psi_h_LR = line.comm.allreduce(df.fem.assemble_scalar(df.fem.form(fx * ufl.dx)), op = MPI.SUM)
bc_right = df.fem.dirichletbc(psi_h_LR,right_dof, Omega)

psi_h = ufl.TrialFunction(Omega)
v = ufl.TestFunction(Omega)

# weak form of 1st-order problem failed
# Raising to 2nd-order problem
psi_hdx = psi_h.dx(0)
g = fx.dx(0)

# Variational form of 2nd-order ODE
F = psi_hdx.dx(0) * v * ufl.dx
F += g * v * ufl.dx
a, L = ufl.system(F)


problem = LinearProblem(a=a,L=L,bcs = [bc_left,bc_right], petsc_options_prefix="cumulative_integral_problem",petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) 
Fh = problem.solve() # 2nd-order problem results incorrect
print(Fh.x.array)
[inf inf inf inf inf inf inf inf inf inf inf]

where I tried to raise the order of the ODE by differentiating both sides before defining a weak form, however this gave infinity everywhere. If you have the time, please let me know if you’ve tried solving the variational form of this problem and if you got similar results.

For the time being, I will stick to the latter process you showed me for what I am attempting to do. I’ll just need to figure out extracting the values along the lower boundary of the square for my original problem and cumulatively integrating that selection. Surely there is a way to do this, I’ll just need to find it. Thank you!

I did try looking over what you had linked, but the material felt too complicated for me to comprehend. I’ve not heard of a restriction operator before, and the problem in the example was well beyond most problems I’ve seen before. So, I decided to set this material aside for future reference, and keep searching around.

Doing some further searching, I came across THIS PROBLEM. Now that I have seen how to cumulatively integrate an interpolated expression on a 1D mesh, I think that what I now need to do to accomplish the same thing for a 2D mesh is quite similar to what @Ramiro_Irastorza was trying to do when going from the parent mesh to a sub-mesh. For my specific problem, I wanted to, in an iterative procedure, do the following

  1. Find the solutions to a pair of problems on a parent mesh
  2. Use the solutions to obtain a new field
  3. Cumulatively integrate that field on the boundaries
  4. Let the results of those cumulative integrals be the boundary values to a new problem

I think what I need to do in order for 3 to work is take the expression interpolated onto the 2D space, and then interpolate that onto a 1D sub-space. Focusing just on the southern boundary of the square mesh, once the original function that was interpolated onto the full domain has been interpolated onto a subspace on the boundary, I think I should be able to do the same cumulative integration; that is what I will attempt to do from here. If you have any thoughts on my plan of action, or you think that this will not work, please feel free to share them with me!

Tagging @sbhasan so that they can see this as well. I believe I have found the other necessary step for interpolating from the parent 2D mesh to 1D submesh which pertains to the parent mesh’s lower boundary. Instead of interpolate, I needed to use interpolate_nonmatching which Dokken provided code for an example regarding a 3D to 2D case HERE. Putting everything together, and also testing the interpolation of Fx onto the original parent mesh’s lower boundary via dirichletbc as this is something that I need to do for my problem, I have the following code tested and running on Dolfinx version 0.9.0

from mpi4py.MPI import COMM_WORLD as comm
import numpy as np
import ufl
from dolfinx.fem import assemble_scalar,dirichletbc,create_interpolation_data,form,Function,functionspace, locate_dofs_geometrical
from dolfinx import plot
from dolfinx.mesh import (
    create_submesh,
    create_unit_square,
    create_rectangle,
    exterior_facet_indices,
    locate_entities,
    locate_entities_boundary,
)
import pyvista as pv
import matplotlib.pyplot as plt
Nx, Ny = 5,5
TYPE = "Lagrange"
ORDER = 2
LABEL_DOFS = "NO"

mesh = create_rectangle(comm,[[0.0,0.0],[np.pi,np.pi]],[Nx,Ny])
tdim = mesh.topology.dim
mesh.topology.create_connectivity(tdim-1,tdim)

def ref_func(x):
    return np.cos(x[0]) + np.sin(x[1]) #This will be cos(x) on the lower boundary


V = functionspace(mesh, (TYPE, ORDER))
f = Function(V)
f.interpolate(ref_func)

# Visualize interpolation of original onto domain

grid = pv.UnstructuredGrid(*plot.vtk_mesh(V))
grid.point_data["f"] = f.x.array.real
grid.set_active_scalars("f")
plotter = pv.Plotter()
plotter.add_mesh(grid, show_edges=True)

if LABEL_DOFS == "YES":
    dof_coords = V.tabulate_dof_coordinates()
    dof_indices = V.dofmap.index_map.local_to_global(np.arange(V.dofmap.index_map.size_local))
    dof_labels = [str(dof_index) for dof_index in dof_indices]

    plotter.add_points(dof_coords,
        color="blue",
        render_points_as_spheres=False,
        point_size=10,
        label="DOFs",
    )
    plotter.add_point_labels(dof_coords,
        dof_labels,
        font_size=10,
        point_color="blue",
        point_size=10,
        always_visible=True,
    )
    
plotter.view_xy()
plotter.show_grid()
plotter.show()
plotter.screenshot("Interpolated_2D_expr.png")

# Locate boundary entities
def lower_boundary_locator(x):
    truth_arr = np.isclose(x[1],0)
    return truth_arr
boundary_cells = locate_entities_boundary(mesh, tdim-1, lower_boundary_locator)

# Create submesh on the boundary
boundary_submesh, parent_cells, vertex_max, node_map = create_submesh(mesh, tdim-1, boundary_cells) # submesh and parent_cells

# Create functionspace using boundary submesh
V_sub_boundary = functionspace(boundary_submesh, (TYPE, ORDER))
fx = Function(V_sub_boundary)

# interpolate reference function onto sub-domain
fine_mesh_cell_map = boundary_submesh.topology.index_map(boundary_submesh.topology.dim)
num_cells_on_proc = fine_mesh_cell_map.size_local + fine_mesh_cell_map.num_ghosts
cells = np.arange(num_cells_on_proc,dtype = np.int32)
interpolation_data = create_interpolation_data(V_sub_boundary,V,np.arange(num_cells_on_proc,dtype = np.int32),1e-14)

fx.interpolate_nonmatching(f,cells,interpolation_data = interpolation_data)

# Cumulatively integrate fx

x = ufl.SpatialCoordinate(boundary_submesh)
Fx = Function(V_sub_boundary)
Fx_integral = lambda x0: np.hstack([assemble_scalar(form(fx*ufl.conditional(x[0] < x_, 1, 0)*ufl.dx)) for x_ in x0[0]])
Fx.interpolate(Fx_integral)


# Interpolate Fx onto parent mesh's boundary
boundary_dofs = locate_dofs_geometrical(V,lower_boundary_locator)
bc_lower = dirichletbc(Fx, boundary_dofs)

# Test expression interpolated onto boundary
fx_on_boundary = f.x.array[boundary_dofs]
Fx_interpolated_onto_boundary = bc_lower.g.x.array
x_axis = V.tabulate_dof_coordinates()[boundary_dofs][:,0]
sort_idx = np.argsort(x_axis)

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x_axis[sort_idx], fx_on_boundary[sort_idx])
ax.plot(x_axis[sort_idx], Fx.x.array[sort_idx], marker = 'o')
ax.plot(x_axis[sort_idx], Fx_interpolated_onto_boundary[sort_idx])
ax.set_xlabel('$x$')
ax.legend(['$f|_{y=0}$','$Fx_{V_{sub}}$','$Fx_{V_{boundary}}$'])
plt.show()

Looks correct. Thank you all for your help!

1 Like

There is an alternative function for interpolating to the boundary submesh, with:

which uses the information of the parent to submesh mapping. It will be part of the next scifem release (tomorrow or Monday)

2 Likes