Table des matières Python

Aimant permanent : utilisation des intégrales elliptiques

1. Introduction

Ce document présente le calcul du champ magnétique créé par un ou plusieurs aimants permanent. La géométrie est axisymétrique et les aimants sont coaxiaux.

Chaque aimant, possédant une aimantation uniforme, est équivalent à deux faces possédant chaune une charge magnétique surfacique uniforme. Le calcul de l'excitation magnétique H créé par une face d'un aimant se fait par une intégrale double sur cette face, comme expliqué dans Aimant permanent.

Dans Aimant permanent, le calcul de cette intégrale double est fait entièrement de manière numérique, à partir d'un maillage de la face de l'aimant.

Nous présentons ici un calcul de l'intégrale reposant sur une expression de l'intégration par rapport à l'angle, qui fait intervenir des intégrales elliptiques complètes, ce qui permet de ramener le calcul de l'intégrale double à une intégrale simple. L'algorithme de Carlson [1] pour le calcul numérique des intégrales elliptiques permet réduire le temps de calcul par rapport au calcul numérique de l'intégrale double.

2. Calcul du champ magnétique

On considère un ou deux aimants en géométrie axisymétrique, chacun étant caractérisé par une aimantation uniforme. Un point P de l'espace est repéré par ses coordonnées cylindriques (r,z).

deuxAimants.svgFigure pleine page

L'excitation magnétique en un point P est calculé par l'intégrale suivante pour chaque face des aimants :

H(P)=σm4πdisqueQP||QP||3dS(1)

σm est la densité de charge magnétique de la face, égale à σm=M ou σm=-M selon qu'il s'agit de la face nord ou de la face sud de l'aimant d'aimantation M.

Pour expliciter cette intégrale, on peut considérer que θ=0. Notons (r11,z1) les coordonnées cylindriques du point Q situé sur une face.

On a :

QP=(r-r1cos(θ1))ux-r1sin(θ1)uy+(z-z1)uz(2)

L'élément de surface s'écrit dS=r11dr1. Les deux composantes de l'excitation magnétique s'écrivent donc :

Hr(r,z)=σm4π0adr102πdθ1 r1(r-r1cos(θ1))(r2+r12-2rr1cos(θ1)+(z-z1)2)32(3)Hz(r,z)=σm4π0adr102πdθ1 r1(z-z1)(r2+r12-2rr1cos(θ1)+(z-z1)2)32(4)

L'intégrale par rapport à l'angle θ1 peut s'exprimer à l'aide des intégrales elliptiques de première et seconde espèces, définies par :

K(x)=0π/2dθ1-xsin2θE(x)=0π/21-xsin2θdθ

Pour exprimer ces intégrales, définissons les expressions suivantes :

b = z-z1(5)A = (r-r1)2+b2(6)B = (r+r1)2+b2(7)C = r2-r12-b2(8)x = -4rr1A(9)

Voici les expressions des intégrales sur la variable θ1, obtenues grace au logiciel de calcul formel Mathematica :

fr=2r1rCE(x)+BK(x))BAsir>0(10)fr=0sir=0(11)fz=4br1E(x)BA(12)

Les intégrales elliptiques sont calculées au moyen des fonctions ellipe et ellipk du module scipy.special. Ces deux fonctions implémentent l'algorithme itératif de Carlson, qui converge nettement plus vite qu'une méthode de calcul de type méthode des trapèzes.

import numpy as np
from scipy.special import ellipk, ellipe
from matplotlib.pyplot import *

def frfz(r,z,r1,z1):
    b = z-z1
    b2 = b*b
    A = (r-r1)**2+b2
    B = (r+r1)**2+b2
    C = r**2-r1**2-b2
    x = -4*r*r1/A
    E = ellipe(x)
    K = ellipk(x)
    D = B*np.sqrt(A)
    if r!=0:
        fr =  2*r1/r*(C*E+B*K)/D
    else:
        fr=0
    fz = 4*b*r1*E/D
    return fr,fz

frfz = np.frompyfunc(frfz,4,2)
                   

Voici un exemple :

z1 = 0
a = 1
z = 0.1
r = 0.1
N = 100
r1 = np.linspace(0,a,N)
fr,fz = frfz(r,z,r1,z1)
figure()
subplot(211)
plot(r1,fr)
ylabel('fr')
grid()
subplot(212)
plot(r1,fz)
ylabel('fz')
xlabel('r1')
grid()
                   
frfz-1frfz-1.pdf

et un autre à une distance plus petite du disque :

z1 = 0
a = 1
z = 0.01
r = 0.1
N = 500
r1 = np.linspace(0,a,N)
fr,fz = frfz(r,z,r1,z1)
figure()
subplot(211)
plot(r1,fr)
ylabel('fr')
grid()
subplot(212)
plot(r1,fz)
ylabel('fz')
xlabel('r1')
grid()
                   
frfz-2frfz-2.pdf

Le calcul de l'intégrale sur la variable r1 peut se faire par la méthode des trapèzes :

dr = r1[1]-r1[0]
Iz = (np.sum(fz[1:N-1])+0.5*(fz[0]+fz[N-1]))*dr
Ir = (np.sum(fr[1:N-1])+0.5*(fr[0]+fr[N-1]))*dr
M = 1
Hz = M/(4*np.pi)*Iz
Hr = M/(4*np.pi)*Ir
                   
print((Hz,Hr))
--> (0.49496075263083483, 0.02507405131733448)

Voici une fonction qui effectue le calcul du champ en un point de coordonnées (r,z) :

def HrHz(r,z,z1,a,M,N):
    r1 = np.linspace(0,a,N)
    dr = r1[1]-r1[0]
    fr,fz = frfz(r,z,r1,z1)
    Hz = M/(4*np.pi)*(np.sum(fz[1:N-1])+0.5*(fz[0]+fz[N-1]))*dr
    Hr = M/(4*np.pi)*(np.sum(fr[1:N-1])+0.5*(fr[0]+fr[N-1]))*dr
    return Hr,Hz
                   

Voici par exemple le tracé du champ sur l'axe, qui peut être comparé à l'expression exacte :

P = 100
z = np.linspace(1e-3,2,P)
Hz = np.zeros(P)
r = 0
z1 = 0
a = 1
M = 1
N = 500
for k in range(P):
    hr,hz = HrHz(r,z[k],z1,a,M,N)
    Hz[k] = hz
def Hz_axe(z,z1,a,M):
    return M/2*((z-z1)/abs(z-z1)-(z-z1)/(a**2+(z-z1)**2)**0.5)
Hz_exact = Hz_axe(z,z1,a,M)
figure()
plot(z,Hz,'b')
plot(z,Hz_exact,'r')
grid()
xlabel('z')
ylabel('Hz')
                   
Hz-1Hz-1.pdf

Le calcul numérique de l'intégrale est incorrect lorsque le point est très proche du disque, car la fonction à intégrer présente dans ce cas des variations très rapides au voisinage de r1=r. Dans le cas ci-dessus, la valeur N=500 n'est pas suffisante pour z-z1=10-3. À l'inverse, pour les points plus éloignés du disque il devrait être possible de réduire la valeur de N. Il est donc judicieux de mettre en place une méthode d'ajustement de N en fonction d'une tolérance. La fonction suivante répète le calcul en augmentant N d'un facteur deux jusqu'à ce que la variation relative de la norme du champ soit inférieure à la tolérance :

def HrHz_adapt(r,z,z1,a,M,Nmin,tol):
    N = Nmin
    Hr1,Hz1 = HrHz(r,z,z1,a,M,N)
    H1 = np.sqrt(Hr1*Hr1+Hz1*Hz1)
    N *= 2
    Hr,Hz = HrHz(r,z,z1,a,M,N)
    H = np.sqrt(Hr*Hr+Hz*Hz)
    while abs((H-H1)/H)>tol:
        H1 = H
        N *= 2
        Hr,Hz = HrHz(r,z,z1,a,M,N)
        H = np.sqrt(Hr*Hr+Hz*Hz)
    return Hr,Hz,N
                    
P = 100
z = np.linspace(1e-3,1,P)
Hz = np.zeros(P)
list_N = np.zeros(P)
r = 0
z1 = 0
a = 1
M = 1
Nmin = 10
tol=1e-2
for k in range(P):
    hr,hz,N = HrHz_adapt(r,z[k],z1,a,M,Nmin,tol)
    Hz[k] = hz
    list_N[k] = N
def Hz_axe(z,z1,a,M):
    return M/2*((z-z1)/abs(z-z1)-(z-z1)/(a**2+(z-z1)**2)**0.5)
Hz_exact = Hz_axe(z,z1,a,M)
figure()
plot(z,Hz,'b')
plot(z,Hz_exact,'r')
grid()
xlabel('z')
ylabel('Hz')
                   
Hz-2Hz-2.pdf

Voici les valeurs de N :

figure()
plot(z,list_N)
xlabel('z')
ylabel('N')
yscale('log')
grid()
                   
N-2N-2.pdf

L'algorithme de calcul de l'intégrale peut être amélioré en remarquant que la somme des N-2 termes peut être réutilisée après avoir multiplié N par 2. Pour cela, il faut définir N comme étant le nombre de sous-intervalles de [0,a] (le nombre de points est N+1). On pose donc :

rkN=kaNk=0,N(13)SN = k=1N-1f(rkN)(14)IN = aN(12f(0)+12f(a)+SN)(15)

IN est l'approximation de l'intégralle pour N sous-intervalles. La somme pour 2N sous-intervalles s'écrit :

S2N=SN+p=0N-1r2p+12N(16)

Pour implémenter cet algorithme, il faut écrire une fonction qui prend en argument la somme SN/2 et renvoie la somme SN et l'approximation de l'intégrale IN :

                     
def HrHz_partiel(r,z,z1,a,M,N,sum_fr,sum_fz):
    dr = a/N
    r1 = np.arange(1,N,2)*dr
    fr,fz = frfz(r,z,r1,z1)
    sum_fr += np.sum(fr)
    sum_fz += np.sum(fz)
    Hz = M/(4*np.pi)*(sum_fz*dr)
    Hr = M/(4*np.pi)*(sum_fr*dr)
    return Hr,Hz,sum_fr,sum_fz
                     

Pour démarrer l'itération à un N quelconque, il faut aussi une fonction qui fait le calcul complet de la somme :

def HrHz_complet(r,z,z1,a,M,N):
    fr0,fz0 = frfz(r,z,0,z1)
    fra,fza = frfz(r,z,a,z1)
    dr = a/N
    r1 = np.arange(1,N)*dr
    fr,fz = frfz(r,z,r1,z1)
    sum_fr = np.sum(fr)+0.5*(fr0+fra)
    sum_fz = np.sum(fz)+0.5*(fz0+fza)
    Hz = M/(4*np.pi)*(sum_fz*dr)
    Hr = M/(4*np.pi)*(sum_fr*dr)
    return Hr,Hz,sum_fr,sum_fz
                     

La fonction suivante effectue les calculs intératifs en partant de N=10.

def HrHz_iter(r,z,z1,a,M,tol):
    N = 10
    Hr1,Hz1,sum_fr,sum_fz = HrHz_complet(r,z,z1,a,M,N)
    H1 = np.sqrt(Hr1*Hr1+Hz1*Hz1)
    N *= 2
    Hr,Hz,sum_fr,sum_fz = HrHz_partiel(r,z,z1,a,M,N,sum_fr,sum_fz)
    H = np.sqrt(Hr*Hr+Hz*Hz)
    while abs((H-H1)/H)>tol:
        H1 = H
        N *= 2
        Hr,Hz,sum_fr,sum_fz = HrHz_partiel(r,z,z1,a,M,N,sum_fr,sum_fz)
        H = np.sqrt(Hr*Hr+Hz*Hz)
    return Hr,Hz,N
    
                     

Voici la reprise du calcul précédent :

P = 100
z = np.linspace(1e-3,1,P)
Hz = np.zeros(P)
list_N = np.zeros(P)
r = 0
z1 = 0
a = 1
M = 1
Nmin = 10
tol=1e-2
for k in range(P):
    hr,hz,N = HrHz_iter(r,z[k],z1,a,M,tol)
    Hz[k] = hz
    list_N[k] = N
def Hz_axe(z,z1,a,M):
    return M/2*((z-z1)/abs(z-z1)-(z-z1)/(a**2+(z-z1)**2)**0.5)
Hz_exact = Hz_axe(z,z1,a,M)
figure()
plot(z,Hz,'b')
plot(z,Hz_exact,'r')
grid()
xlabel('z')
ylabel('Hz')
                   
Hz-3Hz-3.pdf

et les valeurs de N :

figure()
plot(z,list_N)
xlabel('z')
ylabel('N')
yscale('log')
grid()
                   
N-3N-3.pdf

3. Lignes de champ d'un aimant

Le calcul du champ créé par un aimant est fait par la fonction suivante, qui renvoie les composantes de H et de B . L'aimant est caractérisé par son rayon a, les côtes de ses faces zs et zn et son aimantation M.

def champAimant(r,z,a,zs,zn,M,mu0=1,tol=1e-2):
    Hr1,Hz1,N1 = HrHz_iter(r,z,zs,a,-M,tol)
    Hr2,Hz2,N2 = HrHz_iter(r,z,zn,a,M,tol)
    Br1 = mu0*Hr1
    Br2 = mu0*Hr2
    if abs(r)<a and z>min(zs,zn) and z<max(zs,zn):
        Bz1 = mu0*(Hz1+M)
        Bz2 = mu0*(Hz2+M)
    else:
        Bz1 = mu0*Hz1 
        Bz2 = mu0*Hz2
    return (Hr1+Hr2,Hz1+Hz2,Br1+Br2,Bz1+Bz2)

La fonction suivante permet de dessiner une flèche au milieu d'une ligne.

def fleche(x,y,sens,long,style='k-'):
    n = len(x)//2
    xa = x[n]
    xb = x[n+sens]
    ya = y[n]
    yb = y[n+sens]
    z = (xb-xa)+1j*(yb-ya)
    phi = np.angle(z)
    a = np.pi/2+np.pi/3
    xc = xb+long*np.cos(phi-a)
    yc = yb+long*np.sin(phi-a)
    xd = xb+long*np.cos(phi+a)
    yd = yb+long*np.sin(phi+a)
    plot([xb,xc],[yb,yc],style)
    plot([xb,xd],[yb,yd],style)
                    

Le tracé d'une ligne de champ de l'excitation magnétique se fait en partant d'un point à proximité d'une face, jusqu'à un point à proximité d'une face ou bien jusqu'à sortie d'un rectangle centré sur l'aimant. Il faut remarquer que le choix de la valeur de M et de μ0 n'a aucune influence sur les lignes de champ.

def ligneH(a,zs,zn,M,ri,zi,sens,rmax,zmax,dmin):
    ds = 0.01*sens
    r = ri
    z = zi
    ligne_z = []
    ligne_r = []
    continuer = True
    while continuer:
        ligne_z.append(z)
        ligne_r.append(r)
        (Hr,Hz,Br,Bz) = champAimant(r,z,a,zs,zn,M)
        H = np.sqrt(Hr**2+Hz**2)
        r += Hr/H*ds                                    
        z += Hz/H*ds       
        if (abs(z-zs) < dmin and abs(r)<a) or (abs(z-zn)< dmin and abs(r)<a) or (abs(r)>rmax) or (abs(z)>zmax) : continuer=False
    return (np.array(ligne_r),np.array(ligne_z))
a=1
M = 1 
zs=-2
zn=2
ds = 0.05
figure(figsize=(8,8))
zmax = 10
rmax = 10
longfleche = 0.2
dmin = 0.1 # distance minimale d'approche des faces
for ri in [0,0.2,0.4,0.6,0.8,1.0]:
    sens = 1
    ligne_r,ligne_z = ligneH(a,zs,zn,M,ri,zn+dmin,sens,rmax,zmax,dmin)
    plot(ligne_z,ligne_r,'b-')
    fleche(ligne_z,ligne_r,sens,longfleche,style='b-')
    plot(ligne_z,-ligne_r,'b-')
    fleche(ligne_z,-ligne_r,sens,longfleche,style='b-')
    sens = -1
    ligne_r,ligne_z = ligneH(a,zs,zn,M,ri,zs-dmin,sens,rmax,zmax,dmin)
    plot(ligne_z,ligne_r,'b-')
    fleche(ligne_z,ligne_r,sens,longfleche,style='b-')
    plot(ligne_z,-ligne_r,'b-')
    fleche(ligne_z,-ligne_r,sens,longfleche,style='b-')
    sens = 1
    ligne_r,ligne_z = ligneH(a,zs,zn,M,ri,zn-dmin,sens,rmax,zmax,dmin)
    plot(ligne_z,ligne_r,'b-')
    fleche(ligne_z,ligne_r,sens,longfleche,style='b-')
    plot(ligne_z,-ligne_r,'b-')
    fleche(ligne_z,-ligne_r,sens,longfleche,style='b-')
axis('equal')
xlabel('z')
ylabel('r')
xlim(-zmax,zmax)
ylim(-rmax,rmax)
plot([-zs,zs,zs,-zs,-zs],[a,a,-a,-a,a],'r-')
grid()
title('Lignes de H')
                     
aimant-ligneHaimant-ligneH.pdf
def ligneB(a,zs,zn,M,ri,zi,sens,rmax,zmax,dmin):
    ds = 0.01*sens
    r = ri
    z = zi
    ligne_z = []
    ligne_r = []
    continuer = True
    while continuer:
        ligne_z.append(z)
        ligne_r.append(r)
        (Hr,Hz,Br,Bz) = champAimant(r,z,a,zs,zn,M)
        B = np.sqrt(Br**2+Bz**2)
        r += Br/B*ds
        z += Bz/B*ds
        if (abs(z-zs) < dmin and abs(r)<a) or (abs(z-zn)< dmin and abs(r)<a) or (abs(r)>rmax) or (abs(z)>zmax) : continuer=False
    return (np.array(ligne_r),np.array(ligne_z))
    
figure(figsize=(8,8))
for ri in [0,0.2,0.4,0.6,0.8,1.0]: 
    sens = 1
    ligne_r,ligne_z = ligneB(a,zs,zn,M,ri,zn+dmin,sens,rmax,zmax,dmin)
    plot(ligne_z,ligne_r,'b-')
    fleche(ligne_z,ligne_r,sens,longfleche,style='b-')
    plot(ligne_z,-ligne_r,'b-')
    fleche(ligne_z,-ligne_r,sens,longfleche,style='b-')
    sens = -1 
    ligne_r,ligne_z = ligneB(a,zs,zn,M,ri,zs-dmin,sens,rmax,zmax,dmin)
    plot(ligne_z,ligne_r,'b-')
    fleche(ligne_z,ligne_r,sens,longfleche,style='b-')
    plot(ligne_z,-ligne_r,'b-')
    fleche(ligne_z,-ligne_r,sens,longfleche,style='b-')
    sens = -1
    ligne_r,ligne_z = ligneB(a,zs,zn,M,ri,zn-dmin,sens,rmax,zmax,dmin)
    plot(ligne_z,ligne_r,'b-')
    fleche(ligne_z,ligne_r,sens,longfleche,style='b-')
    plot(ligne_z,-ligne_r,'b-')
    fleche(ligne_z,-ligne_r,sens,longfleche,style='b-')
axis('equal')
xlabel('z')
ylabel('r')
xlim(-zmax,zmax)
ylim(-rmax,rmax)
plot([-zs,zs,zs,-zs,-zs],[a,a,-a,-a,a],'r-')
grid()
title('Lignes de B')
                     
aimant-ligneBaimant-ligneB.pdf
Références
[1]  W.H. Press, S.A. Teukolsky, W.T. Vetterling, B.P. Flannery,  Numerical recipes, the art of scientific computing,  (Cambridge University Press, 2007)
Creative Commons LicenseTextes et figures sont mis à disposition sous contrat Creative Commons.