Issue with a multidomain heat transfer

Hello everyone. I am trying to model heat transfer with two domains but it seems that the inner domain has its temperature kept constant for some reason ( see picture) . It the computed example it should not be the case as the physical properties of the two domain are the same.

Here is the code for a MWE

from dolfinx import default_scalar_type
from dolfinx.fem import (Constant, dirichletbc, Function, FunctionSpace, 
                         form,  locate_dofs_topological)

from dolfinx.fem.petsc import LinearProblem, assemble_matrix, create_vector, assemble_vector, apply_lifting, set_bc

from dolfinx.io import XDMFFile, gmshio

from dolfinx.mesh import create_unit_square, locate_entities, locate_entities_boundary


from ufl import (SpatialCoordinate, TestFunction, TrialFunction,
                 dx, grad, inner, Measure, dot, jump, FacetNormal, avg, FiniteElement, MixedElement)

from mpi4py import MPI
from petsc4py import PETSc

import numpy as np
import time


def project(function, space):
    p = TrialFunction(space)
    q = TestFunction(space)
    a = inner(p, q) * dx
    L = inner(function, q) * dx
    problem = LinearProblem(a, L, bcs = [])
    return problem.solve()

###############################################################################
################################# PARAMETER ###################################
###############################################################################

# PhysicalProperties
# density (kg.m^-3) 
rho_1 = 8000.0e-6
rho_2 = 8000.0e-6
# capacity (J/Kg.K)
cp_1 = 435.0e6
cp_2 = 435.5e6
# conductivity (W/m.K)
k_1 = 10.0e3
k_2 = 10.0e3

# Boundary condtions --------------------------------------------------------- 
# build plate temperature (K)
T_plate = 353.0
# initial temperature (K)
T0 = 293.0
T0_1 = 293.0
T0_2 = 320.0

# Time  ----------------------------------------------------------------------
TotalTime = 1000.0

t0=time.time()
    
num_steps = 50     # number of time steps
dt = TotalTime / num_steps # time step size
t = 0 # current time 

CYL_MARK = 2
EXT_MARK = 1

FACE_MARK = 1

###############################################################################
################################# Mesh ########################################
###############################################################################

with XDMFFile(MPI.COMM_WORLD, "CYL_IN_BOX_SLICE.xdmf", "r") as xdmf:
    mesh = xdmf.read_mesh(name="Grid")
    ct = xdmf.read_meshtags(mesh, name="Grid")
    
mesh.topology.create_connectivity(mesh.topology.dim, mesh.topology.dim - 1)

with XDMFFile(MPI.COMM_WORLD, "facet_CYL_IN_BOX_SLICE.xdmf", "r") as xdmf:
    ft = xdmf.read_meshtags(mesh, name="Grid")
    
ds = Measure('ds', domain=mesh, subdomain_data=ft)

###############################################################################
################################# MULTIDOMAIN #################################
###############################################################################

RhoCp_values = [rho_1*cp_1,rho_2*cp_2]
Conductivity_values = [k_1,k_2]
Tinit_values = [T0_1,T0_2]

Q = FunctionSpace(mesh, ("DG", 0))
Conductivity = Function(Q)

for i in range(len(Conductivity_values)):
    cells = ct.find(i+1)
    Conductivity.x.array[cells] = np.full_like(cells, Conductivity_values[i], dtype=default_scalar_type)

RhoCp = Function(Q)
for i in range(len(Conductivity_values)):
    cells = ct.find(i+1)
    RhoCp.x.array[cells] = np.full_like(cells, RhoCp_values[i], dtype=default_scalar_type)

T_INIT = Function(Q)
T_INIT.name = "Tinit"
for i in range(len(Tinit_values)):
    cells = ct.find(i+1)
    T_INIT.x.array[cells] = np.full_like(cells, Tinit_values[i], dtype=default_scalar_type)


###############################################################################
################################# FORMULATION #################################
###############################################################################

# Define variational problem
V = FunctionSpace(mesh, ('CG', 1))
u = TrialFunction(V)
v = TestFunction(V)
f = Constant(mesh, PETSc.ScalarType(0))
n = FacetNormal(mesh)


# initialization
Tinit = Function(V)
Tinit.name = "Tinit"
# variable, need to be projected form Q onto V
Tinit = project(T_INIT,V)
Tinit.name = "Tinit" 


xdmf = XDMFFile(mesh.comm, "./RES/Temperature.xdmf", "w")
xdmf.write_mesh(mesh)

Tinit = project(T_INIT,V)
Tinit.name = "Tinit" 

Timp_facets = ft.find(FACE_MARK)
Timp_dofs = locate_dofs_topological(V, mesh.topology.dim - 1, Timp_facets)
bc = dirichletbc(default_scalar_type(T_plate), Timp_dofs, V)

a = RhoCp*u*v*dx 
a += dt*dot(Conductivity*grad(u), grad(v))*dx

L = (RhoCp*Tinit + dt * f) * v * dx

bilinear_form = form(a)
linear_form = form(L)

A = assemble_matrix(bilinear_form, bcs=[bc])
A.assemble()
b = create_vector(linear_form)

solver = PETSc.KSP().create(mesh.comm)
solver.setOperators(A)
solver.setType(PETSc.KSP.Type.PREONLY)
solver.getPC().setType(PETSc.PC.Type.LU)

# Define solution variable, and interpolate initial solution for visualization in Paraview
sol = Function(V)
sol.name = "Temperature"
sol.interpolate(Tinit)

for i in range(num_steps):
    t += dt
    print("current time is " + str(t) + " s")
    print("Completion is " + str(100*(t/TotalTime))+ " %")
    # Update the right hand side reusing the initial vector
    with b.localForm() as loc_b:
        loc_b.set(0)
    assemble_vector(b, linear_form)

    # Apply Dirichlet boundary condition to the vector
    apply_lifting(b, [bilinear_form], [[bc]])
    b.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
    set_bc(b, [bc])

    # Solve linear problem
    solver.solve(b, sol.vector)
    sol.x.scatter_forward()

    # Update solution at previous time step (u_n)
    Tinit.x.array[:] = sol.x.array

    # Write solution to file
    xdmf.write_function(sol, t)
    
xdmf.close()

Not sure if I am missing something …

Any advice would be very appreciated .

Thanks,

Quentin

2 Likes

As you haven’t supplied the mesh, the issue is not reproducible.
Are you sure the mesh is connected at the interface between the materials, as it looks like it is disconnected.
What is for instance the integral dolfinx.fem.assemble_scalar(dolfinx.fem.form(1*ufl.ds(domain=mesh)))? Is it equal to the surface area of the box?

The Mesh was generate as follows :

  1. A CAD was build thanks to the OCC kernel using the script as follows
from OCC.Core.gp import gp_Pnt, gp_Pln, gp_Trsf, gp_Vec, gp_Ax2, gp_Dir

from OCC.Core.BRepBndLib import brepbndlib_Add
from OCC.Core.Bnd import Bnd_Box

from OCC.Core.BRep import BRep_Builder
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_Transform
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse

from OCC.Core.STEPControl import STEPControl_Reader
from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Builder, topods_Compound

from OCC.Extend.TopologyUtils import TopologyExplorer
from OCC.Extend.DataExchange import read_step_file, write_step_file

def SliceCAD_TwoBoxes(CAD_NAME,SLICING_DIRECTION,adim_height1,adim_height2):
    
    adim_height2_lim = 1.0 - adim_height1
    
    if ( adim_height2 >= adim_height2_lim):
        print("Second slice will not work")
        
    model = read_step_file(str(CAD_NAME))
    bounding_box = get_bounding_box(model)

    min_point, max_point = bounding_box.CornerMin(), bounding_box.CornerMax()

    xmin = min_point.X()
    ymin = min_point.Y()
    zmin = min_point.Z()

    xmax = max_point.X()
    ymax = max_point.Y()
    zmax = max_point.Z()

    Lx = ( xmax - xmin )
    Ly = ( ymax - ymin )
    Lz = ( zmax - zmin )

    H_X = adim_height1*xmin + (1.0 - adim_height1)*xmax
    H_Y = adim_height1*ymin + (1.0 - adim_height1)*ymax
    H_Z = adim_height1*zmin + (1.0 - adim_height1)*zmax


    base_point = gp_Pnt(0, 0, 0)

    # Define the Box and associated tranformations
    box_builder = BRepPrimAPI_MakeBox(base_point, Lx, Ly ,Lz)
    box_shape = box_builder.Shape()

    if( SLICING_DIRECTION == "X"):
        translation_vector1 = gp_Vec(H_X, ymin, zmin)
        translation_vector2 = gp_Vec(H_X-( 1.0 + adim_height2)*Lx, ymin, zmin)
    elif (SLICING_DIRECTION == "Y"):
        translation_vector1 = gp_Vec(xmin, H_Y, zmin)
        translation_vector2 = gp_Vec(xmin, H_Y-( 1.0 + adim_height2)*Ly, zmin)
    elif (SLICING_DIRECTION == "Z"):
        translation_vector1 = gp_Vec(xmin, ymin, H_Z)
        translation_vector2 = gp_Vec(xmin, ymin, H_Z-( 1.0 + adim_height2)*Lz)

    #  translate the boxes
    translation_transform1 = gp_Trsf()
    translation_transform1.SetTranslation(translation_vector1)
    box_transformed_builder1 = BRepBuilderAPI_Transform(box_shape, translation_transform1)
    box_transformed_shape1 = box_transformed_builder1.Shape()

    translation_transform2 = gp_Trsf()
    translation_transform2.SetTranslation(translation_vector2)
    box_transformed_builder2 = BRepBuilderAPI_Transform(box_shape, translation_transform2)
    box_transformed_shape2 = box_transformed_builder2.Shape()


    # Create a boolean cut operation
    cut_operation1 = BRepAlgoAPI_Cut(model, box_transformed_shape1)
    TEMP = cut_operation1.Shape()

    cut_operation2 = BRepAlgoAPI_Cut(TEMP, box_transformed_shape2)
    SLICE = cut_operation2.Shape()

    write_step_file(SLICE,str(CAD_NAME).split(".")[0] +"_SLICE.step")

base_point = gp_Pnt(0, 0, 0)

# Define the Box and associated tranformations
box_builder = BRepPrimAPI_MakeBox(base_point, 10.0, 10. ,10.0)
box_shape = box_builder.Shape()

axis = gp_Ax2(gp_Pnt(5.0, 5.0, 0.0), gp_Dir(0, 0, 1))  
radius = 2.0
height = 10.
cylinder_shape = BRepPrimAPI_MakeCylinder(axis, radius, height).Shape()

compound = TopoDS_Compound()
builder = BRep_Builder()
builder.MakeCompound(compound)
builder.Add(compound, box_shape)
builder.Add(compound, cylinder_shape)

write_step_file(compound,"CYL_IN_BOX.step")

SliceCAD_TwoBoxes("CYL_IN_BOX.step","Z",0.1,0.5)

  1. Then the mesh is obtained using gmsh as follows
CADNAME = "CYL_IN_BOX_SLICE.step"
gmsh.initialize()
gmsh.model.add("MODEL")
gmsh.merge(CADNAME)

LINES = gmsh.model.getEntities(dim=1)
SURFACES = gmsh.model.getEntities(dim=2)
VOLUMES = gmsh.model.getEntities(dim=3)

LINES_1 = [] 
SURFACES_1 = []

for Volume in VOLUMES:
    xmin, ymin, zmin, xmax, ymax, zmax = gmsh.model.getBoundingBox(3,Volume[1])
    surfaces = gmsh.model.getEntitiesInBoundingBox(xmin, ymin, zmin, xmax, ymax, zmax, dim=2)
    curves = gmsh.model.getEntitiesInBoundingBox(xmin, ymin, zmin, xmax, ymax, zmax, dim=1)

    if (Volume[1] == 2):
        for curve in curves:
            LINES_1.append(curve[1])
        for surface in surfaces:    
            SURFACES_1.append(surface[1])
        H_MIN = np.min([(xmax-xmin),(ymax-ymin),(zmax-zmin)],axis=0)
    
    elif( Volume[1] == 1):
        H_MAX = np.min([(xmax-xmin),(ymax-ymin),(zmax-zmin)],axis=0)

# =============================================================================
# gmsh.model.addPhysicalGroup(1, LINES_1,  1, name="PART_INTERFACE_CURVES") 
# 
# gmsh.model.addPhysicalGroup(2, SURFACES_1,  1, name="PART_INTERFACE_FACE")
# 
# gmsh.model.addPhysicalGroup(3, [1], 1, name="PART")
# gmsh.model.addPhysicalGroup(3, [2], 2, name="POWDER")
# =============================================================================

for line in LINES:
    gmsh.model.addPhysicalGroup(1, [line[1]],  line[1], name="LINE_" + str(line[1]))

for surface in SURFACES:
    gmsh.model.addPhysicalGroup(2, [surface[1]],  surface[1], name="SURFACE_" + str(surface[1]))

for volume in VOLUMES:
    gmsh.model.addPhysicalGroup(3, [volume[1]],  volume[1], name="SURFACE_" + str(volume[1]))

SURF_TO_ADAPT = [1,2,6]

gmsh.option.setNumber("Mesh.MeshSizeMin", 0.05)
gmsh.option.setNumber("Mesh.MeshSizeMax", 100.0)
gmsh.option.setNumber("Mesh.Algorithm", 6)

gmsh.model.mesh.field.add("Distance", 1)
gmsh.model.mesh.field.setNumbers(1, "CurvesList", LINES_1)
gmsh.model.mesh.field.setNumber(1, "Sampling", 100)

gmsh.model.mesh.field.add("Threshold", 2)
gmsh.model.mesh.field.setNumber(2, "InField", 1)
gmsh.model.mesh.field.setNumber(2, "SizeMin", 0.05*H_MIN)
gmsh.model.mesh.field.setNumber(2, "SizeMax", 3.0*H_MAX)
gmsh.model.mesh.field.setNumber(2, "DistMin", 0.1)
gmsh.model.mesh.field.setNumber(2, "DistMax", 2.0)
gmsh.model.mesh.field.setNumber(2, "Sigmoid", 1.0)

gmsh.model.mesh.field.add("Distance", 3)
gmsh.model.mesh.field.setNumbers(3, "SurfacesList", SURF_TO_ADAPT)
gmsh.model.mesh.field.setNumber(3, "Sampling", 100)

gmsh.model.mesh.field.add("Threshold", 4)
gmsh.model.mesh.field.setNumber(4, "InField", 3)
gmsh.model.mesh.field.setNumber(4, "SizeMin", 0.05*H_MIN)
gmsh.model.mesh.field.setNumber(4, "SizeMax", 3.0*H_MAX)
gmsh.model.mesh.field.setNumber(4, "DistMin", 0.1)
gmsh.model.mesh.field.setNumber(4, "DistMax", 2.0)
gmsh.model.mesh.field.setNumber(4, "Sigmoid", 1.0)

gmsh.model.mesh.field.add("Min", 5)
gmsh.model.mesh.field.setNumbers(5, "FieldsList", [2, 4])

gmsh.model.mesh.field.setAsBackgroundMesh(5)

gmsh.model.mesh.generate(3)

gmsh.write(str(CADNAME).split(".")[0] + ".msh")
gmsh.clear()
gmsh.finalize()


###############################################################################
############################ WRITE FOR FENICS #################################
###############################################################################

# write mesh in
MAILLAGE = meshio.read(str(CADNAME).split(".")[0] + ".msh")

MESH_LINE = create_mesh(MAILLAGE, "line", prune_z=False)
meshio.write("line_" + str(CADNAME).split(".")[0] + ".xdmf", MESH_LINE)

MESH_TRI = create_mesh(MAILLAGE, "triangle", prune_z=False)
meshio.write("facet_" + str(CADNAME).split(".")[0] + ".xdmf", MESH_TRI)

MESH_TET = create_mesh(MAILLAGE, "tetra", prune_z=False)
meshio.write(str(CADNAME).split(".")[0] + ".xdmf", MESH_TET)

with the create_mesh is the one that can be found in the tutoriels

the

dolfinx.fem.assemble_scalar(dolfinx.fem.form(1*ufl.ds(domain=mesh)))

gives 525.611402316162, which seems correct as I get 525.611 with filter IntegrateVariables of paraview.

Please do not use Paraview as the reference (as it depends on the same connectivity and will like replicate the issue). Rather consider that your box is a 10x10x10 box (as far as I can tell from your script).
Then the surface area of the sides 6*(10**2)=600, which is very different from what you get.

If fact the domain is sliced so that it is square of 10*10 but actual height is 5

hence the area of external boundaries is 2*(1010) (top bottom) + 40.5*(5*10) ( lateral)

which gives 400 but cylinder radius being r=2, it`s external area is 2pir^2*5= 62.8

interestingly the area given by the assemble scalar is area of external faces + 2 times lateral area of the cylinder. so it seems that the internal cylinder boundary is accounted as an external one and two times

checking the boundary tags, it seems that it is the case.

what would be a way to correct this or in other words, How to connect the meshes at the interface ?

Thanks,

Quentin

With GMSH and their occ operators, you can use boolean fragments to combine the two interfaces. I’ve posted multiple posts illustrating this on the form earlier (search for boolean fragment). You can also have a look at: Electromagnetics example — FEniCSx tutorial

I will have a look thanks a lot

I managed to solve the problem

  1. instead of creating the CAD with OCC, It is easier to embed the object using GMSH

  2. once this is done

gmsh.model.occ.fragment([(3, 1)], [(3, 3)])
gmsh.model.occ.removeAllDuplicates()

Then it is working well.

I think this topic can be closed

@Dokken thanks for the guidance

I really like your weak imposition for heat transfer, which does not have in FEniCSx tutorial. It is very help for me. Thanks.

I have same problem before,
where I plan to connect rubbing elements to brake pad together to construct one physical volume. If I do not connect, there are many physcial volumes.

Then I use
shell = gmsh.model.occ.fuse([(3, 70 + i)], [(3, rub_list[i + 1])], 71 + i)

It is also convenient, can combine two volumes to one.