Surface impedance boundary condition

Hi,

I have trouble implementing a SIBC for an eddy current problem. Especially my code fails when I use the CGSolver. The error that I get looks like this:

ZGETRI::info = 1
ZGETRF::info = 1

I have made three options in my code: use SIBC, use reduced vector potential (A = A_0 + A_eddy), and use CGSolver. The options use SIBC does not work with use CGSolver. I hope that you can help to find my mistake.

The code:

from ngsolve import *
from netgen.occ import *
import numpy as np
import netgen.gui

use_SIBC = True
use_reduced_potential = True
use_CGSolver = True #False # using CGSolver is not working with SIBC. Why?

# inner sphere 
sphere_in = Sphere(Pnt(0,0,0), 1)
sphere_in.mat("sphere inner")
sphere_in.solids.name = "sphere inner"
sphere_in.faces.name = "sphere inner"
sphere_in.faces.maxh = 0.3

# outer domain 
air = Sphere(Pnt(0,0,0), 5)
air.solids.name = "air"
air.mat("air")

if use_SIBC:    
    geo = OCCGeometry((air - sphere_in))  # Subtract sphere from air domain (only solve in air)
else:
    air = air - sphere_in
    geo = OCCGeometry(Glue([air, sphere_in]))

# 2. Create Mesh and Define Finite Element Space
mesh = geo.GenerateMesh(maxh=1)
if use_SIBC == False:
    mesh.BoundaryLayer(".*", [0.002, 0.004], material="sphere inner", domains="sphere inner") # add mesh layers
mesh.Curve(5)
mesh = Mesh(mesh)
#Draw(mesh, clipping = { "pnt" : (0,1,0), "vec" : (0,0,-1)})
#input("Press Enter to continue...")

# Use H(curl) elements for vector potential A
import math
fes = HCurl(mesh, order=2, nograds = False, complex=True)

A = fes.TrialFunction()
v = fes.TestFunction()

# Define Material Properties
mu_0 = 4 * np.pi * 1e-7 # Permeability of free space
sigma_in = 5.8e7 # Conductivity of copper
omega = 2 * np.pi * 50 # Frequency (50 Hz)

mu_r = { "air" : 1, "sphere inner": 1}
sigma = {"air": 1e-4, "sphere inner": sigma_in}
mu_0 = 4*math.pi*1e-7
nu_coef = [ 1/(mu_0*mu_r[mat]) for mat in mesh.GetMaterials() ]
sigma_coef = [sigma[mat] for mat in mesh.GetMaterials() ]
eta_coef = [ 1j*omega*sigma[mat] for mat in mesh.GetMaterials() ]

sigma = CoefficientFunction(sigma_coef)
nu = CoefficientFunction(nu_coef)
eta = CoefficientFunction(eta_coef)

# Define Bilinear Form (Weak Formulation) and the Right-Hand Side (Excitation Term)
A_s = CoefficientFunction((-y/2, x/2, 0)) # Impressed vector potential --> uniform B-field in z-direction: B_s=(0,0,1)
B_s = CoefficientFunction((0, 0, 1))
a = BilinearForm(fes, symmetric=False)  #a = BilinearForm(fes, symmetric=True, condense=True)
f = LinearForm(fes)
if use_reduced_potential: # solving for reduced vector potential 
    if use_SIBC:
        a += (1/mu_0) * ( curl(A) * curl(v) * dx + 1e-6*curl(A)*curl(v)*dx )  # Curl-curl term in air
        a += sqrt(1j*omega*sigma_in/mu_0) * A.Trace() * v.Trace() * ds(definedon=mesh.Boundaries("sphere inner")) # SIBC on sphere surface    
        f += sqrt(1j*omega*sigma_in/mu_0) * A_s * v.Trace() * ds(definedon=mesh.Boundaries("sphere inner")) # Excitation term
    else:
        a += SymbolicBFI(eta*A*v + nu*curl(A)*curl(v) + 1e-6*nu*curl(A)*curl(v)) #a += eta*u*v*dx + nu*curl(u)*curl(v)*dx 
        f += eta*A_s*v*dx 
else:  # solving for full vector potential 
    if use_SIBC:
        a += (1/mu_0) * ( curl(A) * curl(v) * dx + 1e-6*curl(A)*curl(v)*dx )  # Curl-curl term in air
        a += sqrt(1j*omega*sigma_in/mu_0) * A.Trace() * v.Trace() * ds(definedon=mesh.Boundaries("sphere inner")) # SIBC on sphere surface    
        H_s = mesh.MaterialCF({}, default = B_s/mu_0) # External field
        f += H_s*curl(v) * dx
    else:
        a += SymbolicBFI(eta*A*v + nu*curl(A)*curl(v) + 1e-6*nu*curl(A)*curl(v)) #a += eta*u*v*dx + nu*curl(u)*curl(v)*dx 
        H_s = mesh.MaterialCF({}, default = B_s/mu_0) # External field
        f += H_s*curl(v) * dx

if use_CGSolver:
    c = Preconditioner(a, type="bddc")
    """ Preconditioners:
        Jacobi (local) and block Jacobi
        Direct solvers using sparse factorization (direct)
        Geometric multigrid with different block-smoothers (multigrid)
        Algebraic multigrid preconditioner (h1amg)
        p-version element-level BDDC (bddc) """


# Solve the System
gfu = GridFunction(fes)
print("Solving...")
with TaskManager():
    a.Assemble()
    f.Assemble()
    if use_CGSolver:
        solver = CGSolver(mat=a.mat, pre=c.mat)
        gfu.vec.data = solver * f.vec 
    else:
        gfu.vec.data = a.mat.Inverse(freedofs=fes.FreeDofs()) * f.vec  

#Draw(gfu, mesh, "Magnetic Vector Potential")
#input("Press Enter to continue...")

if use_reduced_potential:
    B_eddy = curl(gfu) 
else:
    B_eddy = curl(gfu) - B_s
Draw(B_eddy.Norm(), mesh, 'B-field eddy')
input("Press Enter to continue...")

Lapack complains that you want to invert a singular matrix

That’s because you have a curl-curl term, but no L2-term. I guess you want to use the regularization as 1e-6*A*v*dx (instead of '1e-6*curl(A)*curl(v)*dx`).

Joachim

1 Like

Thanks Joachim, that makes sense. It works nicely with your suggestion.

If the solid metal sphere is replaced with a spherical metal shell and the skin depth is not small compared with the shell thickness the SIBC has to be replaced, for example see this reference https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=4305690
I’m not sure how to implement this condition (eq. 11 and 12 below) in NGSolve. Do you have any similar reference case? Would it work using the .Other() operator if the inside of the shell is not meshed?

I think you can mesh the inner and outer part as volume domains and impose periodic boundary conditions on the surfaces. Then define a space on the inner part and one on the outer part and impose the condition by using that you can evaluate both fields on both boundaries using a periodic space.

Thanks! If I understand correctly, the inner and outer surfaces need to have connections like in this example: 2.12 Periodic Spaces — NGS-Py 6.2.2406 documentation

I’ve tried to make the connections but have not succeeded. Here is my code:

from ngsolve import *
from netgen.occ import *
from ngsolve.webgui import Draw

sph1 = Sphere(Pnt(0,0,0), r=1)
sph2 = Sphere(Pnt(0,0,0), r=0.9)
sph1 = sph1 - sph2
#sph1.faces[0].Identify(sph2.faces[0], "ud0",  IdentificationType.PERIODIC)

sph = Glue([sph1, sph2])
sph.faces[0].col = (.5,.5,.5)
sph.faces[1].col = (0,0,1)
sph.faces[0].Identify(sph.faces[1], "periodic", IdentificationType.PERIODIC)

geo = OCCGeometry(sph)
mesh = geo.GenerateMesh(maxh=0.25)
mesh = Mesh(mesh).Curve(3)

plist = []
for pair in mesh.ngmesh.GetIdentifications():
    plist += list(mesh.vertices[pair[0].nr-1].point) + [0]
    plist += list(mesh.vertices[pair[1].nr-1].point) + [0]
print(plist)
Draw(mesh, objects=[{"type" : "lines", "position" : plist, "name": "identification", "color": "purple"}]);

if the transformation between the faces is not pure translation you have to give the transformation manually:

trf = gp_Trsf.Scale((0,0,0), 0.9)
sph.faces[0].Identify(sph.faces[1], "periodic", IdentificationType.PERIODIC,
                      trf)

Thanks, I added the transformation but from what I can see in the attached plot, the connections don’t go to the closest point from one surface to the other.

from ngsolve import *
from netgen.occ import *
from ngsolve.webgui import Draw

sph1 = Sphere(Pnt(0,0,0), r=1)
sph2 = Sphere(Pnt(0,0,0), r=0.9)
sph1 = sph1 - sph2

sph = Glue([sph1, sph2])
sph.faces[0].col = (.5,.5,.5)
sph.faces[1].col = (0,0,1)
trf = gp_Trsf.Scale((0,0,0), 0.9)
sph.faces[0].Identify(sph.faces[1], "periodic", IdentificationType.PERIODIC, trf)

geo = OCCGeometry(sph)
mesh = geo.GenerateMesh(maxh=0.1)
mesh = Mesh(mesh)#.Curve(3)

plist = []
for idx, pair in enumerate(mesh.ngmesh.GetIdentifications()):
    if idx > 100:
        break
    plist += list(mesh.vertices[pair[0].nr-1].point) + [0]
    plist += list(mesh.vertices[pair[1].nr-1].point) + [0]
#print(plist)
Draw(mesh, objects=[{"type" : "lines", "position" : plist, "name": "identification", "color": "white"}]);

It is working, you just need to remove the + [0], this was for 2d to add the z = 0 coordinate for 3d plotting.

from ngsolve import *
from netgen.occ import *
from ngsolve.webgui import Draw

sph1 = Sphere(Pnt(0,0,0), r=1)
sph2 = Sphere(Pnt(0,0,0), r=0.9)
sph1 = sph1 - sph2

sph = Glue([sph1, sph2])
sph.faces[0].col = (.5,.5,.5)
sph.faces[1].col = (0,0,1)
trf = gp_Trsf.Scale((0,0,0), 0.9)
sph.faces[0].Identify(sph.faces[1], "periodic", IdentificationType.PERIODIC, trf)

geo = OCCGeometry(sph)
mesh = geo.GenerateMesh(maxh=0.1)
mesh = Mesh(mesh)#.Curve(3)

plist = []
for idx, pair in enumerate(mesh.ngmesh.GetIdentifications()):
    if idx > 100:
        break
    plist += list(mesh.vertices[pair[0].nr-1].point)
    plist += list(mesh.vertices[pair[1].nr-1].point)
#print(plist)
Draw(mesh, objects=[{"type" : "lines", "position" : plist, "name": "identification", "color": "white"}]);

Great! Thank you Christopher.

I’m not sure if I’m on the right track with the two domains. There are likely some errors in my code but I’m not sure if it is already in my geometry definition or later with the FES. One warning I get is “used dof inconsistency”. Here is my code:

from ngsolve import *
from netgen.occ import *
from ngsolve.webgui import Draw
import numpy as np

metal_outer_r = 1 
inner_t = 0.1
metal_inner_r = metal_outer_r - inner_t

outer =  Sphere(Pnt(0,0,0), r=3).mat("outer")
outer.faces.name = "outer"

sph1 = Sphere(Pnt(0,0,0), r=metal_outer_r)
sph1.faces.name = "metal outside"
sph1.maxh = 0.3

sph2 = Sphere(Pnt(0,0,0), r= metal_inner_r).mat("sphere inner")
sph2.faces.name = "metal inside"
sph2.maxh = 0.3

outer = outer - sph1
sph1 = sph1 - sph2 - outer # Just want the surface (No mesh inside the shell.). Is this correct?
sph1.mat("outer")

sph = Glue([sph1, sph2, outer]) 
sph.faces[0].col = (.5,.5,.5)
sph.faces[1].col = (0,0,1)
sph.faces[2].col = (1,0,0)
trf = gp_Trsf.Scale((0,0,0), metal_inner_r/metal_outer_r)
sph.faces[0].Identify(sph.faces[1], "periodic", IdentificationType.PERIODIC, trf)

geo = OCCGeometry(sph)
mesh = geo.GenerateMesh(maxh=0.5)
mesh = Mesh(mesh).Curve(3)

plist = []
for idx, pair in enumerate(mesh.ngmesh.GetIdentifications()):
    plist += list(mesh.vertices[pair[0].nr-1].point) 
    plist += list(mesh.vertices[pair[1].nr-1].point) 
Draw(mesh, objects=[{"type" : "lines", "position" : plist, "name": "identification", "color": "white"}]);

# Define Material Properties
mu_0 = 4 * math.pi * 1e-7 # Permeability of free space
sigma_in = 5.8e7 # Conductivity of copper
omega = 2 * math.pi * 1 # Frequency (50 Hz)

mu_r = { "outer" : 1, "sphere inner": 1, "default": 1}
sigma = {"outer": 1e-4, "sphere inner": 1e-4, "default": 1e-4}
mu_0 = 4*math.pi*1e-7
nu_coef = [ 1/(mu_0*mu_r[mat]) for mat in mesh.GetMaterials() ]
sigma_coef = [sigma[mat] for mat in mesh.GetMaterials() ]
eta_coef = [ 1j*omega*sigma[mat] for mat in mesh.GetMaterials() ]

sigma = CoefficientFunction(sigma_coef)
nu = CoefficientFunction(nu_coef)
eta = CoefficientFunction(eta_coef)

# wave number and surface impedances
k = (-1j*sigma_in*mu_0*omega)**(1/2)
Z_S = -1j*omega*mu_0/k/np.tan(k*inner_t)
Z_T = -1j*omega*mu_0/k/np.sin(k*inner_t)

import math
order = 3 #
fes_in = Periodic(HCurl(mesh, order=order, nograds = False, complex=True, definedon="sphere inner"))
fes_out = Periodic(HCurl(mesh, order=order, nograds = False, complex=True, definedon="outer"))
fes = FESpace([fes_in, fes_out])

A_in, A_out = fes.TrialFunction()
v_in, v_out = fes.TestFunction()

# Source term
B_s = CoefficientFunction((0, 0, 1))
H_s = mesh.MaterialCF({}, default = B_s/mu_0) # Magnetization or external field

a = BilinearForm(fes) 
f = LinearForm(fes)

a += (1/mu_0) * (curl(A_in) * curl(v_in) + 1e-6*A_in*v_in) * dx(definedon=mesh.Materials("sphere inner"))
a += (1/mu_0) * (curl(A_out) * curl(v_out) + 1e-6*A_out*v_out) * dx(definedon=mesh.Materials("outer"))

# Boundary terms
a += -1j*omega/(Z_S**2 - Z_T**2)*(Z_S * A_out.Trace() * v_out.Trace() - Z_T * A_in.Trace() * v_in.Trace()) * ds(definedon=mesh.Boundaries("metal outside"))     
a += -1j*omega/(Z_S**2 - Z_T**2)*(Z_S * A_in.Trace() * v_in.Trace() - Z_T * A_out.Trace() * v_out.Trace()) * ds(definedon=mesh.Boundaries("metal inside"))   

# Source terms
f += H_s*curl(v_in) * dx(definedon=mesh.Materials("sphere inner"))
f += H_s*curl(v_out) * dx(definedon=mesh.Materials("outer"))

c = Preconditioner(a, type="bddc")

# Solve the System
gfu = GridFunction(fes)
with TaskManager():
    a.Assemble()
    f.Assemble()
    solver = CGSolver(mat=a.mat, pre=c.mat)
    gfu.vec.data = solver * f.vec 

Draw(curl(gfu.components[1]), mesh)