Understanding my problem (ME/accurate solution?)

I don’t know where to begin, I have been working on a single problem since several years now, and it works well in a certain regime (which I want to extend now), but I am facing numerous difficulties. I am at the point of wondering whether I should change my approach and use RT elements and introducing a new unknown.

In short: I solve for T and V in the equations \nabla \cdot \vec J_U=0 and \nabla \cdot \vec J=0 where \vec J=−\sigma \nabla V−\sigma S \nabla T and \vec J_U = -\kappa \nabla T +TS\vec J +V\vec J. My weak form is:

# Weak form.
J_vector = -sigma * grad(volt) - sigma * Seebeck_tensor * grad(temp)
Fourier_term = dot(-κ * grad(temp), grad(u)) * dx
F_T = Fourier_term
rest_terms = dot(temp * Seebeck_tensor* J_vector, grad(u)) * dx + dot(volt * J_vector, grad(u)) * dx
F_T += rest_terms
F_V = -dot(grad(v), sigma * grad(volt))*dx -dot(grad(v), sigma * Seebeck_tensor * grad(temp))*dx
weak_form = F_T + F_V

which corresponds to \nabla \cdot (\kappa \nabla T ) +\nabla \cdot (T S \vec J)+\nabla \cdot(V\vec J)=0 and \nabla \cdot \vec J=0, where I set Dirichlet boundary conditions for T on 2 boundaries (out of 4) and potentially a Dirichlet b.c. at a single point for V (this does help a bit for Newton method to converge).

Here is the thing, S is a tensor, a 2x2 diagonal matrix with entries S_{xx} and S_{xy}. And it turns out that in the regime where this anisotropy is “small” enough not to perturb T much, then my FEniCSx code runs fine and is able to retrieve some analytical formulae I had derived. The problem arises when the anisotropy in S becomes big enough so that T starts to become impacted. For example, in my code I compute:

assemble_scalar(form(div(J_U) * dx))

which should be 0 since this corresponds to the heat equation I am solving. Well, this number starts to increase (it is about 1e-7 or even 1e-8 in the regime of a small anisotropy, where my code runs fine), it becomes something like -0.03 when anisotropy is big and then Newton method diverges. I did implement a trick to make Newton method converge, by solving many intermediate problems with increasing anisotropy in S, but this doesn’t change noticeably the result, i.e. it helps at converging, but it converges to the same bogus values. (I know I am not computing the L2 norm, maybe I should do it?).

As regards to the volume integral of \nabla \cdot \vec J, it also starts to depart from 0 but it is closer to 0 than its \vec J_U counterpart is.

I do not know whether my solution for T and V is accurate, or if the inaccuracy is due to computing gradients.

What I know is that I do need to compute the volume integral of \vec J^2 and spatial derivatives of \vec J, again, integrated over the whole volume. So, I would need an accurate \vec J, I suppose.

I am willing to introduce new variables such as J, J_U, J_Q (some heat flux) and use RT elements using a ME scheme if it makes things better. I tested rapidly and I got even worse results by implementing J with RT elements, where the div J and div J_U values where a million times greater than in my case, for the regime where my code works fine.

Any pointer or idea is welcome. My full code is gigantic and a MWE cannot be implemented, as these values are only achieved in my specific problem. I don’t mind to post my full code here, it’s just big, and the mesh comes from a (g)msh file.

A few more words. I need to compute quantities that derive from the ones I solve, i.e. I need to post-process as accurately as I can. I need to compute the volume integral of \vec J^2 where \vec J is derived from the solutions that live in CG spaces of degree 5, and similarly for \frac{\partial J_x}{\partial x}. The way I do it is fine up to a certain point, from which I don’t know whether the inaccuracy comes from inaccuracies in my post-processing or due to inaccuracies in the solution itself.

LLM IA tells me I can do a better post-process and project J into a RT space. Would this give me more accurate values of the volume integral of \vec J^2 and of \frac{\partial J_x}{\partial x}?

Have you looked into stable mixed poisson formulations?
See for instance
Mixed formulation for the Poisson equation — DOLFINx 0.9.0 documentation or dolfinx-tutorial/chapter4/mixed_poisson.ipynb at main · jorgensd/dolfinx-tutorial · GitHub

As a general comment, you should build your model step by step. In the example provided, you have not provided enough information about the issues that you are phasing for most people to help you.

If your solution is not satisfactory, a post processing projection usually doesn’t solve the problem (it would just hide the issues).

My advise is to work on the mathematical formulation and discretisation of your problem.
I guess the strong form is written at: Deriving the weak form of my problem, is it correct? - #7 by raboynics
and you should sit down and carefully write up a mixed formulation.

To me it kind of looks like a PNP ( https://pmc.ncbi.nlm.nih.gov/articles/PMC3122111/ ) equation (steady state), where you might be able to draw inspiration from https://arxiv.org/pdf/2409.08746 or other papers solving such problems either with a primal or mixed discretisation.

Thanks a lot dokken, I really appreciate your insights!
Yes, I had seen that mixed poisson formulation, but I had forgotten about it, thanks.

I understand my message was very vague. My code is spread over several files, with several classes, it’s not trivial to understand it within a few seconds or minutes. A MWE does not display this problem (although I guess at some point if the anisotropy is insanely high, it might…).
Such a MWE could be:

import numpy as np
from dolfinx import fem, log
from dolfinx.fem import (Constant, Function, functionspace, assemble_scalar,
                         dirichletbc, form, locate_dofs_geometrical,
                         locate_dofs_topological, Expression, create_matrix, assemble_matrix)                     
from dolfinx.fem.petsc import NonlinearProblem
from dolfinx.nls.petsc import NewtonSolver
from mpi4py import MPI
from petsc4py import PETSc
from petsc4py.PETSc import ScalarType
from ufl import (Measure, SpatialCoordinate,
                 TestFunction, TrialFunction, as_tensor, dot, dx, grad, inner,
                 inv, split, derivative, FacetNormal, div, curl, as_vector, action)
import gmsh
from dolfinx.io import gmshio, VTXWriter
from dolfinx import default_scalar_type
from basix.ufl import element, mixed_element
from dolfinx import log, fem, mesh
from dolfinx.mesh import locate_entities_boundary, meshtags, compute_incident_entities

rho = 1
sigma = 1.0 / rho
κ = 1.8    
    
domain = mesh.create_unit_square(MPI.COMM_WORLD, 8, 8, mesh.CellType.quadrilateral)
x = SpatialCoordinate(domain)

def boundary_D_left(x):
    return np.isclose(x[0], 0)

def boundary_D_bottom(x):
    return np.isclose(x[1], 0)

def T_cold_func(x): 
    return np.full(x.shape[1], 100.0)
def T_hot_func(x): 
    return np.full(x.shape[1], 200.0)
    
# Define ME function space
deg = 3
el = element("CG", domain.topology.cell_name(), deg)
mel = mixed_element([el, el])
ME = functionspace(domain, mel)

u, v = TestFunction(ME)
TempVolt = Function(ME)
temp, volt = split(TempVolt)
dTV = TrialFunction(ME)

s_xx, s_yy = 1e-5, 1e1
Seebeck_tensor = as_tensor([[s_xx, 0], [0, s_yy]])

# Define the boundary conditions
T0 = ME.sub(0)
Q, _ = T0.collapse()
T_hot = Function(Q)
T_hot.interpolate(T_hot_func)
facets_hot = locate_entities_boundary(domain, 1, boundary_D_bottom)
dofs = locate_dofs_topological((T0, Q), 1, facets_hot)
bc_temp_left = dirichletbc(T_hot, dofs, T0)

T1 = ME.sub(0)
Q1, _ = T1.collapse()
T_cold = Function(Q1)
T_cold.interpolate(T_cold_func)
facets_cold = locate_entities_boundary(domain, 1, boundary_D_left)
dofs = locate_dofs_topological((T1, Q1), 1, facets_cold)
bc_temp_right = dirichletbc(T_cold, dofs, T1)
bcs = [bc_temp_left, bc_temp_right]#, bc_volt_left]

tdim = domain.topology.dim
fdim = tdim -1 
domain.topology.create_entities(fdim)
facet_map = domain.topology.index_map(fdim)
num_facets = facet_map.size_local + facet_map.num_ghosts
facet_indices = np.arange(0, num_facets)
facet_values = np.zeros_like(facet_indices, dtype=np.intc)

inj_current_curve = 12
out_current_curve = 13
facet_values[facets_cold] = inj_current_curve
facet_values[facets_hot] = out_current_curve
mt_facets = meshtags(domain, fdim, facet_indices, facet_values)

x = SpatialCoordinate(domain)
dx = Measure("dx", domain=domain)#, subdomain_data=cell_markers)
ds = Measure("ds", domain=domain, subdomain_data=mt_facets)

# Weak form.
J_vector = -sigma * grad(volt) - sigma * Seebeck_tensor * grad(temp)

Fourier_term = dot(-κ * grad(temp), grad(u)) * dx
F_T = Fourier_term
rest_terms = dot(temp * Seebeck_tensor* J_vector, grad(u)) * dx + dot(volt * J_vector, grad(u)) * dx
F_T += rest_terms
F_V = -dot(grad(v), sigma * grad(volt))*dx -dot(grad(v), sigma * Seebeck_tensor * grad(temp))*dx

weak_form = F_T + F_V

Jac = derivative(weak_form,TempVolt,dTV)

def initial_guess_temp(x):
    return np.full(x.shape[1], 100.0)
    
def initial_guess_volt(x):
    return np.zeros(x.shape[1])
         
TempVolt.sub(0).interpolate(initial_guess_temp)
TempVolt.sub(1).interpolate(initial_guess_volt)

# Solve the PDE.
problem = NonlinearProblem(weak_form, TempVolt, J = Jac, bcs=bcs)
solver = NewtonSolver(MPI.COMM_WORLD, problem)
solver.convergence_criterion = "residual"#"incremental"#"residual"
solver.rtol = 1e-10
solver.atol = 1e-10
solver.report = True
solver.max_it = 100

ksp = solver.krylov_solver
opts = PETSc.Options()
option_prefix = ksp.getOptionsPrefix()
opts[f"{option_prefix}ksp_type"] = "preonly"
opts[f"{option_prefix}pc_type"] = "lu"
opts[f"{option_prefix}pc_factor_mat_solver_type"] = "mumps"
ksp.setFromOptions()
log.set_log_level(log.LogLevel.WARNING)    
n, converged = solver.solve(TempVolt)
assert(converged)
print(f'Successfull simulations, temp and volt are found. Number of iterations: {n:d}')

# Current density.
J = functionspace(domain, ("DG", deg-1, (domain.geometry.dim,)))
Je = Function(J)
Je_flux_calculator = Expression(J_vector, J.element.interpolation_points())
Je.interpolate(Je_flux_calculator)

with VTXWriter(MPI.COMM_WORLD, "results/Je_debug_nonlinear_newton.bp", Je, engine="BP4") as vtx:
    vtx.write(0.0)
qdeg = 12

# Joule Heat
joule_heat = assemble_scalar(form(inner(rho * J_vector, J_vector) * dx,
                                    form_compiler_options={"quadrature_degree": qdeg}))

# Bridgman Heat (strong form)
bridgman_heat_strong = assemble_scalar(form(
    -temp * (s_xx * J_vector[0].dx(0) + s_yy * J_vector[1].dx(1)) * dx,
    form_compiler_options={"quadrature_degree": qdeg}))

# L2 norm of div J
divJ_sq = assemble_scalar(form(inner(div(J_vector), div(J_vector)) * dx,
                                form_compiler_options={"quadrature_degree": qdeg}))
div_J_L2 = np.sqrt(divJ_sq)

# L2 norm of div J_U
J_U = -κ * grad(temp) + temp * Seebeck_tensor * J_vector + volt * J_vector
divJU_sq = assemble_scalar(form(inner(div(J_U), div(J_U)) * dx,
                                form_compiler_options={"quadrature_degree": qdeg}))
div_J_U_L2 = np.sqrt(divJU_sq)

Where in my case I get problems when s_yy is about 1 and above. Below this value, I have little to no problem at all.

I have just ran a convergence study, and I get the typical (from what I understand), log-h dependence of the L2 norms in the regime where everything runs fine (for example when s_yy=5e-3). Note that the 2 following plots come from my original code, not the MWE here. But the physics is the same. I think that only the code structure differs (and the mesh/domain).


It’s not very visible but the red curve worsen at the first decrease in h, and then it slowly get better as it should with decreasing h. Nevertheless, this is not a typical behavior from what I understand. And the horrible thing is the value of Bridgman heat, as you can see, its value varies widely. It varies so much that I do not know which one to trust, if any.

In contrast, I get the typical behavior for s_yy = 5e-3, and Bridgman heat and Joule heats are “stable” enough with h.