How to perform convolution integration, or Fourier Transform in Dolfinx?

I am following from the methodology laid out in this paper to solve a special problem, and I am stuck on creating a vector field, \textbf{V}_h, such that it satisfies certain conditions. In order for \textbf{V}_h to be used in subsequent steps, it has to satisfy 2 conditions

\tag{5.13}\oint \textbf{V}_h \cdot \hat{\textbf{n}} ds = 0 i.e. the net flux on the boundary is 0, and
\tag{5.14}\oint \textbf{V}_h \cdot \hat{\textbf{s}}ds = 0 i.e. the net circulation on the boundary is 0.

If the obtained vector field \textbf{V}_h does not satisfy these conditions, then according to the paper, “some adjustment must be made to the value of \textbf{V}_h so that the integral constraints (5.13) and (5.14) are satisfied.” And that gets us to what I want to implement. According to this user on stackexchange, I can try to convolute the divergence of my vector field with a Coulomb field in 2D, and the logic behind it is that it will create a vector field with the same divergence as the original vector field, and I can then subtract that vector field off of my \textbf{V}_h so that it becomes divergence-free, and then doing something similar to subsequently get rid of the curl. So, what I want to do, is take divV_h = ufl.div(V_h) and convolve it with a Coulomb field C = ufl.as_vector( (x[0]/(x[0]**2+x[1]**2), x[1]/(x[0]**2+x[1]**2)) ). Or I could create Dolfinx interpolated quantities of these ufl expressions and work with those, the point is that I want to see how to perform the convolution integrals between the scalar field with the vector field; the convolution will create a new vector field
\textbf{V}_{new} = \nabla \cdot \textbf{V}_h * \Big(\frac{x}{x^2 + y^2}\hat{\textbf{i}} + \frac{y}{x^2 + y^2}\hat{\textbf{j}}\Big)
. Additionally, it is stated that we can make this process much faster and simpler by taking advantage of convolution theorem by using a Fourier transform
F\Big\{\nabla \cdot \textbf{V}_h * \Big(\frac{x}{x^2+y^2}\hat{\textbf{i}} + \frac{y}{x^2+y^2}\hat{\textbf{j}}\Big)\Big\} = F\{\nabla \cdot \textbf{V}_h\} F\Big\{\Big(\frac{x}{x^2+y^2}\hat{\textbf{i}} + \frac{y}{x^2+y^2}\hat{\textbf{j}}\Big)\Big\}
as this lets us simply multiply the two transforms together rather than perform integration, and then we can subsequently inverse transform the product. I would appreciated it if someone could show me how to implement either the convolution, or taking the Fourier Transform of these variables.

To preface my code a bit, it is following the methodology in the paper (where I have excluded the iterative procedure part as it is not relevant to this question), except instead of using the Harmonic-Sine method, I am using Dolfinx for everything involving solving PDEs. The formula solved in the code is \textbf{V}_h = \textbf{V} - \textbf{V}_I, and the code is used to compute \textbf{V}_I by first solving 2 Poisson Equations with homogeneous boundary conditions to obtain \psi_I and \chi_I, and \textbf{V}_I = \textbf{V}_{\psi_I} + \textbf{V}_{\chi_I}= \hat{\textbf{k}}\times \nabla \psi_I + \nabla \chi_I. Here is my code for the creation of \textbf{V}_h given a vector field \textbf{V} = 3(x-y)\hat{\textbf{i}} + (3x-y)\hat{\textbf{j}} on the domain R:[-4\le x \le 4, -4\le y \le 4]

from mpi4py import MPI
import pyvista as pv
import dolfinx as df
import ufl
from petsc4py import PETSc
import numpy as np
import matplotlib.pyplot as plt

Nx,Ny = 60,60 # Number of points in x and y
family = "Lagrange"
order = 1
# overlapping_facets = True # Uncertainty of whether or not degrees of freedom should overlap when setting boundary conditions for each boundary 
Lx, Ly = 4,4

mesh = df.mesh.create_rectangle(MPI.COMM_WORLD, [[-Lx,-Ly],[Lx,Ly]] ,[Nx, Ny])
tdim = mesh.topology.dim
fdim = tdim-1
mesh.topology.create_connectivity(tdim-1, tdim)

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

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

# Extract vertical relative vorticity from wind field (onto scalar space IMPORTANT)
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) # Evaluate vort epression

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

u = ufl.TrialFunction(S)
v = ufl.TestFunction(S)
a = ufl.inner(ufl.grad(u),ufl.grad(v)) * ufl.dx
# f = Vorticity
L = -Z*v*ufl.dx
from dolfinx.fem.petsc import LinearProblem
problem = LinearProblem(a=a,L=L,bcs = [bc], petsc_options_prefix="Streamfunction_I",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
# f = Divergence
L = -d*v*ufl.dx
problem = LinearProblem(a=a,L=L,bcs = [bc], petsc_options_prefix="Velocity_potential_I",petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) 
chi_I = problem.solve()

# Move to 2D vector space to store V_I into 
V_I = df.fem.Function(V)
V_I_expr = df.fem.Expression(ufl.perp(ufl.grad(psi_I)) + ufl.grad(chi_I), V.element.interpolation_points)
V_I.interpolate(V_I_expr)

V_h_true = df.fem.Function(V) # Unique harmonic wind vector
V_h_expr = df.fem.Expression(wind - V_I, V.element.interpolation_points)
V_h_true.interpolate(V_h_expr)  
# V_h = V_h_true.copy() # This will be modified in the iterative procedure.

V_h_values = np.zeros([V.dofmap.index_map.size_local, 3])
V_h_values[:,:mesh.geometry.dim] = V_h_true.x.array.reshape(V.dofmap.index_map.size_local, V.dofmap.index_map_bs)
grid = pv.UnstructuredGrid(*df.plot.vtk_mesh(V))
grid["V_h"] = V_h_values
glyphs = grid.glyph(orient = "V_h", scale = "V_h", factor = .075, tolerance=0.025)
grid.set_active_vectors("V_h")
plotter = pv.Plotter()
plotter.add_mesh(grid, show_edges= False, color = 'white')
plotter.add_mesh(glyphs)
plotter.view_xy()
plotter.show()

where we want to modify the vector field V_h_true seen plotted in the code so that it then becomes divergence-free and curl-free. Additionally, here is the code for computing the net flux and circulation as well as the individual net fluxes and circulations along each of the 4 boundaries

boundaries = [
    (1, lambda x: np.isclose(x[1], -Ly)),
    (2, lambda x: np.isclose(x[0], Lx)),
    (3, lambda x: np.isclose(x[1], Ly)),
    (4, lambda x: np.isclose(x[0], -Lx)),
    ]

facet_indices, facet_markers = [], []
fdim = tdim - 1
for marker, locator in boundaries:
    facets = df.mesh.locate_entities(mesh, fdim, locator)
    facet_indices.append(facets)
    facet_markers.append(np.full_like(facets, marker))
facet_indices = np.hstack(facet_indices).astype(np.int32)
facet_markers = np.hstack(facet_markers).astype(np.int32)
sorted_facets = np.argsort(facet_indices)
facet_tag = df.mesh.meshtags(
    mesh, fdim, facet_indices[sorted_facets], facet_markers[sorted_facets]
)

ds = ufl.Measure("ds", domain=mesh, subdomain_data=facet_tag)

boundary_Vh_flux = []
boundary_Vh_circ = []
boundary_psih_flux = []

for boundary in boundaries:
    boundary_marker = boundary[0]
    
    compiled_form_n = df.fem.form(ufl.dot(ufl.FacetNormal(mesh),V_h)* ds(boundary_marker))
    bf = mesh.comm.allreduce(df.fem.assemble_scalar(compiled_form_n), op = MPI.SUM)
    boundary_Vh_flux.append(bf)

    compiled_form_t = df.fem.form(ufl.dot(ufl.perp(ufl.FacetNormal(mesh)),V_h)* ds(boundary_marker))
    bc = mesh.comm.allreduce(df.fem.assemble_scalar(compiled_form_t), op = MPI.SUM)
    boundary_Vh_circ.append(bc)
    
    #compiled_form = df.fem.form(ufl.dot(ufl.FacetNormal(mesh),ufl.grad(psi_h_0))* ds(boundary_marker))
    #psih_f = mesh.comm.allreduce(df.fem.assemble_scalar(compiled_form), op = MPI.SUM)
    #boundary_psih_flux.append(psih_f)
    

print("fluxes of V_h along 4 boundaries = ", boundary_Vh_flux)
print("net V_h flux = ", sum(boundary_Vh_flux))
print("circulation of $V_h$ along 4 boundaries = ", boundary_Vh_circ)
print("net V_h circulation = ", sum(boundary_Vh_circ))
#print("fluxes of $psi_h$ along 4 boundaries = ", boundary_psih_flux)
#print("net psi_h flux = ", sum(boundary_psih_flux))

where we see that it fails to satisfy both conditions for the net flux and circulation conditions. Once we are finished modifying V_h by subtracting off the convolution (or convolution theorem using Fourier Transform) of the divergence and Coulomb field, we want the resulting vector field to be divergence-free resulting in net flux to approximately vanish. Then, performing the same procedure to make the vector field curl-free should cause both the net flux and net circulation to vanish.


Just as an aside, something else I was considering was modifying the variational form to incorporate these net flux and circulation conditions, but it was a bit too advanced for me to figure out how to create the correct form. If I could incorporate these as conditions, then V_h would satisfy these conditions from the get-go.

Hi fea,

I didn’t read all of questions. But I have an idea. Constructing a field \mathbf{V}_h satisfying those conditions from a given function \mathbf{V} seems to be straightforward with few steps. Starting with a edge based elements (e.g. N1curl(1)), once I calculate \oint \mathbf{V}_h \cdot \hat{s}ds, you have a boundary condition and you can construct a function in this space having the same line integral by solving any equation (e.g. inner(u, v) * dx mass! or inner(curl(u), curl(v)) * dx more fancy). Subtracting it, you can make it satisfy the second condition. You can calculate the surface normal integrals on the boundary, you can solve the Laplace equation with those surface charges. Then by subtracting it, you can make it satisfy the first condition (since you subtract a gradient field, the second condition is still intact.).

This method is called ‘lifting’ (or something)?

Best,

Thank you, young, for your response! There are some things I would like to state and ask about your answer below:

  • Firstly, yes, that’s correct. At the heart of my problem, all I am really trying to do is modify the vector field I obtained, \textbf{V}_h, such that is satisfies the two constraints. Oh and just in case, It should also be stated that I don’t want to drastically change the original vector field either i.e. the new one should resemble the old one and its original magnitudes to some degree.

  • Second, I am afraid that what you have suggested in some parts is beyond my comprehension and/or capability. Could you please provide a code snippet which executes just the creation of one of the vector fields we intend to subtract off? I was confused by some of the wording when paired up with the examples provided for the creation of “any equation”, and I do not know how to apply the net flux as a boundary condition (something I stated at the end of my question), so if I could see those 2 things coded out, that should clear up the misunderstandings. I hope that is not asking too much!

fea,
Sure, I can try.

I haven’t work not much on 2D. I am mostly working on 3D. So, I may need to figure out how discrete De Rham complex works on 2D first.

inner(u, v) * dx = 0 with inhomogeneous essential boundary conditions might be the equation to solve to cancel out the tangential part of it.
inner(grad(p), grad(q) * dx = inner(n, V2) * q *ds (it has a natural boundary condition term) may give a function to cancel out the normal components.

Alright, young! Please take your time! And thank you!

In addition, I have created a code snippet that I think might help you to understand what I am misunderstanding about what you have suggested with the boundary condition here:

and creating creating what I am assuming was a variational form:

Here is the code snippet with comments should be helpful:

import dolfinx as df
import ufl
from mpi4py import MPI
from dolfinx.fem.petsc import LinearProblem

Nx,Ny = 60,60 # Number of points in x and y
family = "Lagrange"
order = 1
Lx, Ly = 1,1

mesh = df.mesh.create_rectangle(MPI.COMM_WORLD, [[-Lx,-Ly],[Lx,Ly]] ,[Nx, Ny])
tdim = mesh.topology.dim
fdim = tdim-1
mesh.topology.create_connectivity(tdim-1, tdim) # necessary for creation of boundary conditions
# Define a true functionspace for later usage
V = df.fem.functionspace(mesh, (family, order, (2,))) # 2D Vector function space

# Vector field to correct
x = ufl.SpatialCoordinate(mesh)
vec = df.fem.Function(V)
vec_expr = df.fem.Expression(ufl.as_vector((-x[1]+x[0],x[0]+x[1])),V.element.interpolation_points)
vec.interpolate(vec_expr)

################################################################################
# Correction ###################################################################
################################################################################
# From your first equation which I am assuming was a variational form, inner(u, v) * dx = 0
# forcing term (I have this defined as a 0 vector)
f = df.fem.Constant(mesh,(df.default_scalar_type(0), df.default_scalar_type(0)))

# boundary condition (I do not know how to implement the boundary condition, so this will likely be wrong)
boundary_dofs = df.fem.locate_dofs_topological(V,fdim,df.mesh.exterior_facet_indices(mesh.topology))
net_circulation = df.fem.assemble_scalar(df.fem.form(ufl.dot(ufl.perp(ufl.FacetNormal(mesh)),vec)* ufl.ds))
# the net circulation is a constant, so I'm guessing this would be a Dirichlet boundary condition
net_flux_bc = df.fem.dirichletbc(df.default_scalar_type(net_circulation),boundary_dofs,V.sub(0))

# variational form
u = ufl.TrialFunction(V)
v = ufl.TestFunction(V)
a = ufl.inner(u,v) * ufl.dx # left side is inner(u, v) * dx
L = -ufl.inner(f,v)*ufl.dx # right side is 0
# If you are on Dolfinx 0.9 or earlier, remove the petsc_options_prefix, and I think it should run
problem = LinearProblem(a=a,L=L,bcs = [net_flux_bc], petsc_options_prefix="Create_vector_to_remove_circulation",petsc_options={"ksp_type": "preonly", "pc_type": "lu"}) 
vec_1 = problem.solve()

vec_no_circulation = df.fem.Function(V)
vec_expr = df.fem.Expression(vec-vec_1,V.element.interpolation_points)
vec_no_circulation.interpolate(vec_expr)

################################################################################
# Test #########################################################################
################################################################################

compiled_form_t = df.fem.form(ufl.dot(ufl.perp(ufl.FacetNormal(mesh)),vec_no_circulation)* ufl.ds)
bc = mesh.comm.allreduce(df.fem.assemble_scalar(compiled_form_t), op = MPI.SUM)
print("Net flux of new vector = ",bc)

I can tell that I did not implement what you suggested correctly based on the printed result not being a net flux of 0. Anyways, I look forward to seeing your code snippet as it will show me how you intended for me to set this up!

fea,

I have been working on it today but couldn’t finish it.

Right, I meant that variational form. But, I am thinking about using N1curl(k) elements to cancel out the circulations since tangential components gives Dirichlet boundary conditions.
For a given function f, I can find a function g that has the same tangential components with f on the boundary and solves the weak form inner(g, v) * dx = 0 for every v in N1curl(k)_0.
Then you can see that f' = f - g does not have any circulation on the boundary.

Then, moving to Lagrange (k + 1) elements. I can try to find a function h such that inner(n, grad(h)) = f' on the boundary and solves the weak form inner(grad(h), grad(i)) * dx = 0 for every i in Lagrange (k+1)_0. This is a natural boundary problem.

Then f'' = f' - h is a function satisfying both conditions and not so different from the original function f in some sense.

Best.

fea,

Which function space does V and V_h reside? Is it H(\mathrm{curl}) or something else?

I tried to solve it. And the cancellation isn’t that great (quite bit of residual normal flux and little bit of circulations.) Maybe I need to be more careful about the order of elements and choices between projection or interpolation. (I used all interpolation since it is simpler. But it may be not an optimal choice since I am working with multiple functionspaces.)

V = dolfinx.fem.functionspace(mesh, ('N1curl', 1))
Q = dolfinx.fem.functionspace(mesh, ('CG', 2))

x, y = ufl.SpatialCoordinate(mesh)
f_expr = ufl.as_vector([3. * x - y, 3. * x - y])

n = ufl.FacetNormal(mesh)
t = ufl.as_vector([-n[1], n[0]])

# 1. Suppressing the circulation.
facets = dolfinx.mesh.exterior_facet_indices(mesh.topology)
u_boundary_dofs = dolfinx.fem.locate_dofs_topological(V, fdim, facets)

u, v = ufl.TrialFunction(V), ufl.TestFunction(V)

u_D = dolfinx.fem.Function(V)
u_D.interpolate(dolfinx.fem.Expression(f_expr, V.element.interpolation_points))
bc = dolfinx.fem.dirichletbc(u_D, u_boundary_dofs)

a = ufl.inner(u, v) * ufl.dx
zero_constant = dolfinx.fem.Constant(mesh, [0., 0.])
L = ufl.inner(zero_constant, v) * ufl.dx

# This uh should suppress the circulation.
prob = dolfinx.fem.petsc.LinearProblem(a, L, bcs=[bc], petsc_options_prefix='circulation')
uh = prob.solve()

# But f_expr - uh still has small residual circulation.
dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(t, f_expr - uh) * ufl.ds))

# 2. Suppressing the normal flux.
pinned_dof = np.argmin(np.linalg.norm(Q.tabulate_dof_coordinates(), axis=1))

p_D = dolfinx.fem.Function(Q)
p_D.x.array[:] = 0.
bc = dolfinx.fem.dirichletbc(p_D, np.asarray([pinned_dof]))

p, q = ufl.TrialFunction(Q), ufl.TestFunction(Q)

a = ufl.inner(ufl.grad(p), ufl.grad(q)) * ufl.dx
L = ufl.inner(n, f_expr - uh) * q * ufl.ds

# This ph should suppress the normal flux.
prob = dolfinx.fem.petsc.LinearProblem(a, L, bcs=[bc], petsc_options_prefix='1')
ph = prob.solve()

# I found that cancellation are not great.
dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(t, f_expr - uh - ufl.grad(ph)) * ufl.ds)), dolfinx.fem.assemble_scalar(dolfinx.fem.form(ufl.inner(n, f_expr - uh - ufl.grad(ph)) * ufl.ds))

Sorry this response is a bit lengthy, but I wanted to respond to some things from both of your replies in this message; I left an important question at the end.

Ah! That’s what I suspected, but I am very much an amateur with this stuff, so I wanted to clarify. Thank you!

In the first code snippet in my question, I had the original vector \textbf{V} stored into the variable wind, and this was defined in the functionspace V = df.fem.functionspace(mesh, (family, order, (2,))) where family = "Lagrange" and order=1. So it is stored in a Lagrangian vector space. As for the vector I obtained and now want to modify, \textbf{V}_h which is stored into the variable v_h_true, I had that defined in the same functionspace V. I have not heard of an H(\text{curl}) space before, I’ll assume it refers to this

Either way, if you think it or any other space will be more optimal to use, then feel free to use that instead wherever you see fit. It looks like this functionspace gave very good results for the net circulation.

This code snippet examines the circulation and flux values along each boundary, and might be helpful to diagnose what went wrong:

V_h = f_expr - uh - ufl.grad(ph)
boundaries = [
    (1, lambda x: np.isclose(x[1], -Ly)),
    (2, lambda x: np.isclose(x[0], Lx)),
    (3, lambda x: np.isclose(x[1], Ly)),
    (4, lambda x: np.isclose(x[0], -Lx)),
    ]

facet_indices, facet_markers = [], []
fdim = tdim - 1
for marker, locator in boundaries:
    facets = dolfinx.mesh.locate_entities(mesh, mesh.topology.dim-1, locator)
    facet_indices.append(facets)
    facet_markers.append(np.full_like(facets, marker))
facet_indices = np.hstack(facet_indices).astype(np.int32)
facet_markers = np.hstack(facet_markers).astype(np.int32)
sorted_facets = np.argsort(facet_indices)
facet_tag = dolfinx.mesh.meshtags(
    mesh, mesh.topology.dim-1, facet_indices[sorted_facets], facet_markers[sorted_facets]
)

ds = ufl.Measure("ds", domain=mesh, subdomain_data=facet_tag)

boundary_Vh_flux = []
boundary_Vh_circ = []
boundary_psih_flux = []

for boundary in boundaries:
    boundary_marker = boundary[0]
    
    compiled_form_n = dolfinx.fem.form(ufl.dot(n,V_h)* ds(boundary_marker))
    bf = mesh.comm.allreduce(dolfinx.fem.assemble_scalar(compiled_form_n), op = MPI.SUM)
    boundary_Vh_flux.append(bf)

    compiled_form_t = df.fem.form(ufl.dot(t,V_h)* ds(boundary_marker))
    bc = mesh.comm.allreduce(dolfinx.fem.assemble_scalar(compiled_form_t), op = MPI.SUM)
    boundary_Vh_circ.append(bc)

print("fluxes of V_h along 4 boundaries = ", boundary_Vh_flux)
print("net V_h flux = ", sum(boundary_Vh_flux))
print("circulation of $V_h$ along 4 boundaries = ", boundary_Vh_circ)
print("net V_h circulation = ", sum(boundary_Vh_circ))

The results show that the circulations are nearly equal and opposite on the boundaries, but there is a greater disparity between flux values on the boundary. I hope this is of some help to you.

Lastly, are you able to visualize f'' and compare it to f? I tried to do so using Pyvista by interpolating both expressions onto a 2D Lagrangian functionspace plotting_space = df.fem.functionspace(mesh,("Lagrange",2,(2,))), and both vector fields looked drastically different. Maybe the interpolation of a ufl expression containing functions defined in different functionspaces is problematic.

fea,

Sure, you chose the continuous Lagrange element. But in the paper you mentioned, which function space do they use? They also formulate the equation to a weak form with some Sobolev space?

Now, I am guessing that asking the exactly same Dirichlet or Neumann boundary conditions is too much (or restrictive). I will try to impose only same circulation and the normal flux.

Best,

fea,

I got another idea.

You can solve the curl curl equation on N1curl(1) with a uniform J. You just need to control total flux of J to control the circulation of the solution on the boundary.

You can solve the Poisson equation on CG(2) with a uniform charge density. You can control total flux by controlling the total charge.

Sum of these two will cancel the circulation and flux of a given function.

Best,

young,

The paper is quite a bit more primitive than that. I will try to provide as much context as I can below to maybe help you determine an optimal setup. I left a couple questions at the very end as well.

The paper uses a double Fourier sine series defined in equations (2.13) where F^{^-1} is defined in equation (2.7), to obtain an analytic solution to the Poisson equation for the dataset they have chosen. I opted to use Dolfinx to solve this for me as using their method resulted in a strong Gibbs phenomena on the boundary which I did not know how to suppress, and I defined everything in a simple 1st-order Lagrangian functionspace.
(Also, ignore the dataset they used, I tested the vector field chosen in THIS 2014 PAPER which effectively solves the same problem as this paper and copies the methodology used.)

Here is a condensed process starting from the beginning to obtaining \textbf{V}_h if it helps:

we want to find \chi and \psi such that (1.2)-(1.6) in the paper are satisfied, and these equations can be rewritten as
\textbf{V} = \textbf{V}_{\chi} + \textbf{V}_\psi = \hat{\textbf{k}}\times \nabla \psi + \nabla \chi
which is the decomposition of a vector field into its irrotational and nondivergent components which follows from Helmholtz decomposition. \psi and \chi satisfy
\nabla^2 \psi = \hat{\textbf{k}}\cdot \nabla \times \textbf{V} = curl(\textbf{V})
\nabla^2 \chi = \nabla \cdot \textbf{V} = div(\textbf{V})
and on the boundary, these scalar potentials must satisfy
\hat{\textbf{s}}\cdot \textbf{V} = \frac{\partial \psi}{\partial n} + \frac{\partial \chi}{\partial s}
\hat{\textbf{n}}\cdot \textbf{V} = -\frac{\partial \psi}{\partial s} + \frac{\partial \chi}{\partial n}
However, we do not know any of the values of \chi or \psi at the boundary, so we cannot apply this boundary condition. That’s where \textbf{V}_h comes in; this vector field is used to determine the values of these scalar potentials on the boundary. We get \textbf{V}_h as a consequence of splitting up the solutions into 2 components, an internal component which satisfies Poisson’s equation with homogeneous Dirichlet boundary conditions, and a harmonic component which satisfies Laplace’s Equation with unknown inhomogeneous Dirichlet boundary conditions. By solving
\nabla^2 \psi_I = curl(\textbf{V})
\psi_I = 0 \ \ \ \ \forall \psi_I \in \partial\Omega
\nabla^2 \chi_I = div(\textbf{V})
\chi_I = 0 \ \ \ \ \forall \psi_I \in \partial\Omega
we get \textbf{V}_h by \textbf{V}_h = \textbf{V} - \textbf{V}_I, where \textbf{V}_I = \hat{\textbf{k}}\times \nabla \psi_I + \nabla \chi_I. According to the paper on page 101, there is another hidden boundary condition which \textbf{V}_h needs to satisfy in order for it to yield accurate information about the boundary values of \psi_h and \chi_h, and that is the harmonic wind, \textbf{V}_h, “is nondivergent and irrotational, the total flux and circulation of \textbf{V}_h along the boudnary region R must vanish. Thus,
(5.13)\oint \hat{\textbf{n}} \cdot \textbf{V}_h = \int_0^{L_x}\hat{\textbf{-j}} \cdot \textbf{V}_h dx+ \int_0^{L_y} \hat{\textbf{i}} \cdot \textbf{V}_h dy + \int_{L_x}^0 \hat{\textbf{j}} \cdot \textbf{V}_h (-dx) + \int_{L_y}^0 \hat{\textbf{-i}} \cdot \textbf{V}_h (-dy) = 0
(5.14)\oint \hat{\textbf{s}} \cdot \textbf{V}_h = \int_0^{L_x}\hat{\textbf{i}} \cdot \textbf{V}_h dx+ \int_0^{L_y} \hat{\textbf{j}} \cdot \textbf{V}_h dy + \int_{L_x}^0 \hat{\textbf{-i}} \cdot \textbf{V}_h (-dx) + \int_{L_y}^0 \hat{\textbf{-j}} \cdot \textbf{V}_h (-dy) = 0.
If the integration of \textbf{V}_h along the boundary does not satisfy (5.13) and (5.14), some adjustment must be made to the value of \textbf{V}_h so that the integral constraints (5.13) and (5.14) are satisfied.”
That’s all the paper says, it does not give specifics on how one would adjust the vector field. I considered incorporating (5.13) and (5.14) as an another boundary condition to the Poisson Equation when setting up the variational form, but I do not know how to implement it.

Also, showing you the figures in the 2014 paper so that you have an idea of what to work towards should also help:

If you examine the streamlines in figure 1 of the vector field \textbf{V} = 3(x-y)\hat{\textbf{i}}, 3x-y \hat{\textbf{j}} (fig 1a) and its true components \textbf{V}_{\psi_I} (fig 1b), \textbf{V}_{\chi_I} (fig 1c), and \textbf{V}_h (fig 1d) which satisfies the net circulation and flux being 0

and here is what they were able to obtain by solving the 2 Poisson Equations subjected to homogeneous boundary conditions to obtain \textbf{V}_{\psi_I} = \hat{\textbf{k}} \times \nabla \psi_I (fig 3b) and \textbf{V}_{\chi_I} = \nabla \chi_I (fig 3c), and \textbf{V}_h = \textbf{V} - \textbf{V}_{\psi_I} - \textbf{V}_{\chi_I} (fig 3d):

When I computed everything and plotted it, the results in fig 3 looked very similar to mine. Unfortunately, they did not show how they adjusted their harmonic vector field.

Let me know what the results were if you have them.

I do not know what J is, and while I did read up a little bit on electrostatics some months ago, I will still have to guess what “uniform charge density” and its associated term “total charge” are pertaining to when I translate it to my problem.

If the idea is to look at the energy in the system, one concept that comes up in the paper is using the mean kinetic energy KE_{mean} = \frac{1}{2}\int_\Omega |\textbf{V}_h|^2 dx and how much it changes after modification; ideally we would want the change to be approximately 0.

So it sounds like it’s now \textbf{h} = solution_{curl} + solution_{poisson},
\textbf{V}_h - \textbf{h} = \textbf{V}_{h_1}, and \oint \textbf{V}_{h_1} \cdot \hat{\textbf{s}} = \oint \textbf{V}_{h_1} \cdot \hat{\textbf{n}} = 0.
What are the “curl equation” and Poisson equation? Did we define them earlier?

fea,
I am still trying to understand your last reply. But it seems like the paper is about how to perform the Helmholtz decomposition for a given vector field into solenoida, irrotational, and harmonic parts on a bounded domain in \mathbb{R}^2. Is it true?

\mathbf{V} = \nabla \times \mathbf{A} + \nabla \psi + \mathbf{V}_h

For the unbounded \mathbb{R}^3, there is no uniqueness problem. And the Fourier transform is a typical way to prove this theorem I think.

For a bounded domain, you need to specify boundary conditions for this decomposition.
To me, natural boundary conditions seem to be an obvious choice. In order to determine \mathbf{A}, \psi, and \mathbf{V}_h for a given \mathbf{V},

  1. The curl curl equation \nabla \times \nabla \times \mathbf{A} = \nabla \times \mathbf{V} and the Coulomb gauge \nabla \cdot \mathbf{A} = 0 on \Omega. and \hat{\mathbf{n}} \times \nabla \times \mathbf{A} = \hat{\mathbf{n}} \times \mathbf{V} on \partial \Omega.
  2. The Poisson equation \nabla \cdot \nabla \psi = \nabla \cdot \mathbf{V} on \Omega and \mathbf{n} \cdot \nabla \psi = \mathbf{n} \cdot \mathbf{V} on \partial \Omega. You need to handle the null space here with all natural boundary conditions.
  3. \mathbf{V}_h is the rest of it.
    For a simply connected domain (like a rectangle), \mathbf{V}_h would be 0 though.

Let me work on things.

Best,

1 Like

I looked around and saw that some literature inherently includes the harmonic vector, \textbf{V}_h, in the Helmholtz decomposition equation where \nabla \cdot \textbf{V}_h = \nabla \times \textbf{V}_h = 0. This makes the standard equation for a vector field \textbf{F}
\textbf{F} = \nabla \times \textbf{A} + \nabla \psi + \textbf{V}_h
which gives the curl curl equation
\nabla \times \textbf{F} = \nabla \times \nabla \times \textbf{A} + 0 + 0
and the Poisson Equation
\nabla\cdot \textbf{F} = 0 + \nabla^2 \psi + 0
So, what you have shown should still be correct for a Helmholtz decomposition formulation, sorry for the misunderstanding. The way it is formulated in the 2 papers I am using appear to formulate it a bit differently, and the way it is formulated in the paper should not give 0 for \textbf{V}_h. I will assume you are using a different methodology right now than in the papers, (looks like electrostatics) and in this case perhaps \textbf{V}_h ends up becoming 0. Either way, I will let you get to it. Sorry again for the misunderstanding in my previous message!

Please take your time, and thank you in advance!