Following from this tutorial, it looks like the way to assign boundary conditions in scenarios where the boundary conditions overlap at the corners is to let the degrees of freedom overlap, and that this is also true for different boundary conditions. I wanted to make sure of whether or not that is always the case, or if I have misinterpreted the code.
Right now, I am currently struggling with a Helmholtz decomposition problem, and I implemented a conditional for whether or not to have the degrees of freedom to interpolate our boundary condition to should overlap or not. To help explain my code, it solves for \psi_I and \chi_I to obtain \textbf{V}_h = \textbf{V} - \hat{\textbf{k}}\times \nabla \psi_I + \nabla \chi_I (in case there is confusion on \psi_I, I believe \hat{\textbf{k}}\times \nabla \psi_I = \nabla \times \textbf{A}). Here is my code (the boundary conditions are handled in the class):
from mpi4py import MPI
import dolfinx as df
import ufl
from petsc4py import PETSc
import numpy as np
from dolfinx.fem.petsc import LinearProblem
class bc_setter:
def __init__(self):
self.x = ufl.SpatialCoordinate(mesh)
self.axis = 0 # initial axis of integration
#self.t = ufl.perp(ufl.FacetNormal(mesh))
# initial x_c
self.x_c = df.fem.Constant(mesh, PETSc.ScalarType(0.0))
# define all boundary facets for integration
self.boundaries = [
(1, lambda x: np.isclose(x[1], -Ly)), # Southern boundary
(2, lambda x: np.isclose(x[0], Lx)), # Eastern boundary
(3, lambda x: np.isclose(x[1], Ly)), # Northern boundary
(4, lambda x: np.isclose(x[0], -Lx)), # Western boundary
]
facet_indices, facet_markers = [], []
for marker, locator in self.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)
self.facet_tag = df.mesh.meshtags(
mesh, fdim, facet_indices[sorted_facets], facet_markers[sorted_facets]
)
self.ds = ufl.Measure("ds", domain=mesh, subdomain_data=self.facet_tag) # boundary integration variable
# initialize integral as self variable
self.compiled_form = df.fem.form(-ufl.dot(n,V_h)
* ufl.conditional(self.x[0]<self.x_c,1,0) * ufl.ds)
# Function called by interpolate() to evaluate integral
def cumul_int_expr(self,x):
out_arr = np.zeros(x.shape[1])
for i in range(x.shape[1]):
self.x_c.value = x[self.axis][i]
cumul_int_x_eval = df.fem.assemble_scalar(self.compiled_form)
cixe_refined = mesh.comm.allreduce(cumul_int_x_eval, op = MPI.SUM)
out_arr[i] = cixe_refined
return out_arr
def set_psi_bcs(self):
psi_h_bcs = []
bf = []
for boundary in self.boundaries:
boundary_marker = boundary[0]
# create constant from previous psih_s endpoint value
if boundary_marker == 1:
psi_h_s0 = 0
else:
psi_h_s0 = psi_h_s.x.array[df.fem.locate_dofs_topological(S,fdim,self.facet_tag.find(boundary_marker-1))][np.argsort(S.tabulate_dof_coordinates()[df.fem.locate_dofs_topological(S,fdim,self.facet_tag.find(boundary_marker-1))][:,self.axis])][endpoint_index]
if boundary_marker%2 == 0:
self.axis = 1 # y-axis
s = self.x[1]
else:
self.axis = 0 # x-axis
s = self.x[0]
if boundary_marker <= 2: # positive integration direction
endpoint_index = -1
self.compiled_form = df.fem.form(-ufl.dot(n,V_h)
* ufl.conditional(s<self.x_c,1,0) * self.ds(boundary_marker))
else: # negative integration direction
endpoint_index = 0
self.compiled_form = df.fem.form(-ufl.dot(n,V_h)
* ufl.conditional(s<self.x_c,0,1) * self.ds(boundary_marker))
# ---- psi_h(s) = psi_h(s_0) - int_{s_0}^s dot(V_h,n) ds ----
psi_h_s = df.fem.Function(S)
psi_h_s.interpolate(self.cumul_int_expr)
psi_h_s.x.array[:] = psi_h_s0 + psi_h_s.x.array[:]
compiled_form_n = df.fem.form(ufl.dot(ufl.FacetNormal(mesh),V_h)* self.ds(boundary_marker))
bf.append(mesh.comm.allreduce(df.fem.assemble_scalar(compiled_form_n), op = MPI.SUM)) # append boundary flux
# interpolate psi_h(s) values onto boundary dofs
if overlapping_facets:
interpolation_facets = self.facet_tag.find(boundary_marker)
interpolation_dofs = df.fem.locate_dofs_topological(S,fdim,interpolation_facets)
else:
integration_facets = self.facet_tag.find(boundary_marker)
integration_dofs = df.fem.locate_dofs_topological(S,fdim,integration_facets)
sorted_indices = np.argsort(S.tabulate_dof_coordinates()[integration_dofs][:,self.axis])
dof_to_remove = integration_dofs[sorted_indices][endpoint_index]
A = integration_dofs.tolist()
A.remove(dof_to_remove)
interpolation_dofs = np.array(A,dtype = np.int32)
psi_h_bcs.append( df.fem.dirichletbc(psi_h_s, interpolation_dofs) )
# Check if V_h used is nondivergent : FAILS
print("fluxes of V_h along 4 boundaries = ", bf)
print("net V_h flux (supposed to be ~ 0) = ", sum(bf))
return psi_h_bcs
Nx,Ny = 60,60 # Number of points in x and y
family = "Lagrange"
order = 1
overlapping_facets = False
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 normal and tangential unit vectors
n = ufl.FacetNormal(mesh)
t = ufl.as_vector([-n[1], n[0]])
V_space = df.fem.functionspace(mesh, (family, order, (2,))) # Vector space
wind = df.fem.Function(V_space)
wind.interpolate(lambda x: (3*(x[0]-x[1]),3*x[0]-x[1]))
S = df.fem.functionspace(mesh, (family, order)) # Scalar space
# ---- Poisson Equation: psi_I ----
# del^2 psi_I = -curl(wind)
# psi_I = 0 on boundary
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) # Z = curl(wind)
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
L = -Z*v*ufl.dx
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()
# ---- Poisson Equation: chi_I ----
# del^2 chi_I = -divergence(wind)
# chi_I = 0 on boundary
d_expr = df.fem.Expression(ufl.div(wind),S.element.interpolation_points)
d = df.fem.Function(S)
d.interpolate(d_expr) # Evaluate divergence(wind)
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()
# ---- V_h = wind - V_I ----
V_I = df.fem.Function(V_space)
V_I_expr = df.fem.Expression(ufl.perp(ufl.grad(psi_I)) + ufl.grad(chi_I), V_space.element.interpolation_points)
V_I.interpolate(V_I_expr)
V_h_true = df.fem.Function(V_space)
V_h_expr = df.fem.Expression(wind - V_I, V_space.element.interpolation_points)
V_h_true.interpolate(V_h_expr) # This variable will store the true V_h to compare to later
V_h = V_h_true.copy() # This variable will change during runtime (right now it is set to V_h_true)
# ---- Laplace Equation: psi_h ----
# del^2 psi_I = 0
# psi_h(s) = psi_h(s_0) - int_{s_0}^s dot(V_h, n) ds on boundary
bc_obj = bc_setter()
psi_h_s = bc_obj.set_psi_bcs()
f = df.fem.Constant(mesh, df.default_scalar_type(0)); L = -f*v*ufl.dx
problem = LinearProblem(a = a, L = L, bcs = psi_h_s, petsc_options_prefix="Streamfunction_h",petsc_options={"ksp_type": "preonly", "pc_type": "lu"})
psi_h_0 = problem.solve()
Which is the correct approach? Do I let the boundary dofs interpolated to for each boundary condition overlap, or not?