Table des matières Python

Racines d'un polynôme

1. Introduction

Cette page montre comment obtenir numériquement les racines d'un polynôme de degré n, défini par :

P(x)=k=0nakxk=a0+a1x+anxn(1)

Les coefficients peuvent être complexes et, dans tous les cas, on recherche les racines complexes. Un polynôme de degré n possède n racines complexes mais il peut y avoir des racines multiples. Par exemple, le polynôme P(x)=(x-1)3 admet trois racines égales.

On peut citer comme application la recherche des pôles d'une fonction de transfert d'un système linéaire et la réduction en éléments simples de cette fonction de transfert, ou plus généralement la réduction en éléments simples d'une fraction rationnelle.

2. Méthodes numériques

2.a. Évaluation du polynôme et de ses dérivées

Il s'agit de calculer P(x) pour une valeur de x donnée (complexe en général) le plus efficacement possible et en minimisant les erreurs d'arrondis. L'évaluation repose sur la factorisation suivante [1]:

P(x)=a0+x(a1+a2x+anxn-1) =a0+x(a1+x(a2+a3x+annxn-2)) = =a0+x(a1+x(a2+x(a3+x())))

qui conduit à l'algorithme récursif suivant :

pan pan-1+px pan-2+px pa0+px
import numpy as np
			

La fonction suivante effectue l'évaluation d'un polynôme dont les coefficients sont donnés dans une liste ou un tableau numpy.ndarray contenant des nombres complexes :

def evalPol(a,x):
	k=len(a)-1
	p = a[k] #a_n
	k -= 1
	while k>=0:
		p = a[k]+p*x
		k -= 1
	return p
	
			

Voici un exemple avec le polynome P(x)=1+x+x2 :

a=[1+1j*0,1+1j*0,1+1j*0]
p = evalPol(a,1j)
			
print(p)
--> 1j

Considérons la dérivée du polynôme, définie par :

P'(x)=a1+2a2x+3a3x2+nanxn-1(2)

L'évalution de cette dérivée se fait de manière similaire à l'évaluation du polynôme, en multipliant chaque coefficient an par n :

def evalDerivPol(a,x):
	k=len(a)-1
	p = k*a[k] #(n-1)na_n
	k -= 1
	while k>=1:
		p = k*a[k]+p*x
		k -= 1
	return p
	
			
a=[1+1j*0,1+1j*0,1+1j*0]
p = evalDerivPol(a,1j)
			
print(p)
--> (1+2j)

Considérons la dérivée seconde du polynôme :

P''(x)=2a2+2(3)a3x+(n-1)nanxn-2(3)
def evalDeriv2Pol(a,x):
	k=len(a)-1
	p = (k-1)*k*a[k] #na_n
	k -= 1
	while k>=2:
		p = (k-1)*k*a[k]+p*x
		k -= 1
	return p
	
			
a=[1+1j*0,1+1j*0,1+1j*0]
p = evalDeriv2Pol(a,1j)
			
print(p)
--> (2+0j)
a=[0+0j,0+0j,0+0j,1+0j]
p = evalDeriv2Pol(a,1)
			
print(p)
--> (6+0j)

2.b. Méthode de Laguerre

La méthode de Laguerre [1] permet d'obtenir la valeur approchée de la racine d'un polynôme la plus proche d'une valeur initiale donnée. Elle repose sur l'utilisation des dérivées première et seconde du polynôme. Notons x0,x1,xn-1 les n racines du polynôme puis sa factorisation :

P(x)=(x-x0)(x-x1)(x-xn-1)(4)

La dérivée du polynôme (définie plus haut) peut se calculer à partir de cette expression (comme si x et les racines étaient réelles). On obtient aisément :

P'(x)P(x)=1x-x0+1x-x1++1x-xn-1(5)

La dérivation de ce rapport conduit à :

(P'(x)P(x))2-P''(x)P(x)=1(x-x0)2+1(x-x1)2++1(x-xn-1)2(6)

Soit x une valeur (complexe) supposée proche de la racine x0, constituant une première estimation de cette racine. Si elle en est suffisamment proche, on peut considérer que la différence entre x est toutes les autres racines est pratiquement la même :

x-x0=d(7)x-xk=e,k=1,2,n-1(8)

D'après la relation (5), on a alors :

G(x)=P'(x)P(x)=1d+n-1e(9)

et d'après la relation (6) :

H(x)=(P'(x)P(x))2-P''(x)P(x)=1d2+n-1e2(10)

L'élimination de e de ces deux dernières équations conduit à une équation de degré 2 pour d, dont les solutions sont :

d=nG(x)±(n-1)(nH(x)-G(x)2)(11)

La solution dont le module est le plus petit est retenue. La nouvelle estimation est x-d. On répète itérativement ces opérations jusqu'à obtenir une valeur de d dont le module est inférieur à une certaine tolérance. Les deux fonctions suivantes implémentent cet algorithme :

def iteration(a,x):
	n = len(a)-1
	p = evalPol(a,x)
	if p==0: return 0
	pp = evalDerivPol(a,x)
	ppp = evalDeriv2Pol(a,x)
	G = pp/p
	H = G*G-ppp/p
	sq = np.sqrt((n-1)*(n*H-G*G))
	z1 = G+sq
	z2 = G-sq
	if np.absolute(z2) > np.absolute(z1):
		z1 = z2
	d = n/z1
	return d
	
def laguerre(a,x,tol,maxiter=100):
	d = iteration(a,x)
	if d==0:
		return x
	x = x-d
	iter = 1
	while np.absolute(d)>tol:
		d = iteration(a,x)
		if d==0:
			return x
		x = x-d
		iter += 1
		if iter > maxiter:
			raise Exception("Non convergence")
	return x
	
	
				

Afin de tester cette fonction, nous écrivons une fonction qui renvoie les coefficients d'un polynôme de degré 3 dont les trois racines sont données :

def polynome(x0,x1,x2):
	return np.array([-x0*x1*x2,x0*x2+x0*x1+x1*x2,-(x0+x1+x2),1],dtype=np.complex128)
				

Voici un test. La valeur initiale de x est nulle.

x0 = 1
x1 = 2+3*1j
x2 = 2-3*1j
a = polynome(x0,x1,x2)
try:
	x = laguerre(a,0,1e-4)
except:
	print("Non convergence")
	x = 0
				
print(x)
--> np.complex128(1.0000000000000036+0j)

On obtient la racine la plus proche de la valeur initiale (nulle).

2.c. Factorisation

Une fois la racine x0 obtenue (la plus proche de 0 si la valeur d'essai initiale est nulle), on factorise le polynome :

P(x)=(x-x0)(b0+b1x+b2x2+bn-1xn-1)=(x-x0)Q(x)(12)

puis on cherche la racine la plus proche de zéro de Q(x). On procède ainsi itérativement jusqu'à l'obtention des n racines.

Le développement de la forme factorisée permet d'obtenir :

an=bn-1an-1=bn-2-x0bn-1an-2=bn-3-x0bn-2a2=b1-x0b2a1=b0-x0b1a0=-x0b0

On peut donc calculer les coefficients du polynôme Q de la manière suivante :

bn-1=anbn-2=an-1+x0bn-1bn-3=an-2+x0bn-2b1=a2+x0b2b0=a1+x0b1

La fonction suivante effectue la factorisation :

def factorisation(a,x0):
	n = len(a)-1
	b = np.zeros(n,dtype=np.complex128)
	k = n-1
	b[k] = a[k+1]
	k -= 1
	while k>=0:
		b[k] = a[k+1]+x0*b[k+1]
		k -= 1
	return b
				 

La fonction suivante détermine les n racines d'un polynôme :

def racines(a,tol=1e-4):
	n = len(a)-1
	r = []
	for k in range(n-1):
		x = laguerre(a,0,tol)
		r.append(x)
		a = factorisation(a,x)
	r.append(-a[0]/a[1]) # racine d'une polynome de degré 1
	return r
				 

Voici un test :

x0 = 1
x1 = 2+3*1j
x2 = 2-3*1j
a = polynome(x0,x1,x2)
r = racines(a)
				 
print(r[0])
--> np.complex128(1.0000000000000036+0j)
print(r[1])
--> np.complex128(1.9999999999999982+2.999999999999999j)
print(r[2])
--> np.complex128(1.9999999999999982-2.999999999999999j)

En raison des erreurs d'arrondis qui apparaissent lors de la factorisation, [1] recommande de considérer ces premières évaluations des racines et d'appliquer la méthode de Laguerre pour chacune d'elle sur le polynôme de départ afin d'affiner la précision :

def racines2(a,tol=1e-4):
	r = racines(a,tol)
	rac = []
	for x in r:
		x = laguerre(a,x,tol)
		rac.append(x)
	return rac
				  

Voici un test :

x0 = 1
x1 = 2+3*1j
x2 = 2-3*1j
a = polynome(x0,x1,x2)
r = racines2(a)
				 
print(r[0])
--> np.complex128(1+0j)
print(r[1])
--> np.complex128(1.9999999999999998+3j)
print(r[2])
--> np.complex128(1.9999999999999998-3j)

voici un autre exemple :

x0 = -3
x1 = 1+1j
x2 = 1-1j
a = polynome(x0,x1,x2)
r = racines2(a,tol=1e-6)
				 
print(r[0])
--> np.complex128(1+1j)
print(r[1])
--> np.complex128(1-1j)
print(r[2])
--> np.complex128(-3+0j)

Voici un exemple avec des racines multiples :

x0 = 1
x1 = 1
x2 = 2
a = polynome(x0,x1,x2)
r = racines2(a,tol=1e-6)
				 
print(r)
--> [np.complex128(0.9999999755927836+0j), np.complex128(1.0000000294952005+0j), np.complex128(2+0j)]

Voici un exemple avec un polynôme à coefficients complexes :

x0 = -3
x1 = 1+1j
x2 = 1+2j
a = polynome(x0,x1,x2)
r = racines2(a,tol=1e-6)
				 
print(a)
--> array([-3.+9.j, -7.-6.j,  1.-3.j,  1.+0.j])
print(r[0])
--> np.complex128(0.9999999999999998+1.0000000000000002j)
print(r[1])
--> np.complex128(1.0000000000000002+1.9999999999999998j)
print(r[2])
--> np.complex128(-3+0j)

3. Applications

3.a. Réponse indicielle d'un système linéaire

On considère un système linéaire dont la fonction de transfert s'écrit :

H(p)=b0+b1p+b2p2+bmpma0+a1p+a2p2+anpn(13)

La réponse fréquence du système s'obtient en posant p=jω .

Si e(t) et s(t) sont respectivement l'entrée et la sortie du système, l'équation différentielle s'écrit :

a0s+a1dsdt+a2d2sdt2+andnsdt2=b0e+b1dedt+b2d2edt2+amdmedtm(14)

La réponse indicielle est la fonction s(t) lorsque e(t) est un échelon, défini par :

e(t)=0pourt<0e(t)=Epourt0

L'équation différentielle vérifiée par la sortie pour t0 est donc :

a0s+a1dsdt+a2d2sdt2+andnsdt2=b0E(15)

La condition initiale en t=0 pour s(t) correspond au régime permanent pour e=0, c'est-à-dire s(0)=0. L'équation différentielle nécessite n conditions initiales. Les conditions initiales sur les dérivées de s(t) d'ordre 1 à n-1 dépendent de propriétés du système qui n'apparaissent pas dans sa fonction de transfert. On considère ici que ces dérivées sont toutes nulles à l'instant t=0, mais la méthode pourra s'appliquer sans difficulté à des conditions initiales différentes.

Si l'on note p1,p2pn les pôles de la fonction de transfert, c'est-à-dire les racines de son dénominateur, la réponse indicielle s'écrit, dans le cas où il n'y a aucun pôle multiple [2] :

s(t)=b0Ea0+K1ep1t+K2ep2t+Knepnt(16)

Le système est stable si s(t) tend vers b0Ea0 lorsque t , c'est-à-dire lorsque les pôles ont tous une partie réelle strictement négative.

Les conditions initiales permettent d'écrire le système linéaire pour les constantes K1,K2,Kn :

K1+K2+Kn=-b0Ea0p1K1+p2K2+pnKn=0p12K1+p22K2+pn2Kn=0p1n-1K1+p2n-1K2+pnn-1Kn=0

Lorsque le système est stable, on s'intéresse à la forme de la réponse indicielle, en particulier à la présence d'un dépassement ou d'oscillations amorties. La valeur de b0Ea0 est sans importance et on la prendra donc égale à 1.

from numpy.linalg import solve

def reponseIndicielle(a,t,tol=1e-3):
	r = racines2(a,tol)
	n = len(r)
	A = np.zeros((n,n),dtype=np.complex128)
	for j in range(n):
		for i in range(n):
			A[i][j] = r[j]**i
	B = np.zeros(n)
	B[0] = -1
	K = solve(A,B)
	s = 1
	for i in range(n):
		s += K[i]*np.exp(r[i]*t)
	return np.real(s)
					 

Pour tester cette fonction, considérons l'exemple classique du système d'ordre 2 stable suivant :

H(p)=11+2mTp+T2p2(17)

Le discriminant du dénominateur est Δ= 4T2(m2-1) . Si m>1, la réponse est de type amortie sans dépassement. Si m<1, la réponse est pseudo-périodique amortie. La cas m=1 correspond au régime critique et nécessite un traitement spécial puisque dans ce cas il y un pôle double.

from matplotlib.pyplot import *
T = 1
t = np.linspace(0,20*T,1000)
figure()
for m in [0.5,0.8,0.99,2]:
	a=np.array([1,2*m*T,T**2],dtype=np.complex128)
	s = reponseIndicielle(a,t)
	plot(t,s,label="m=%0.2f"%m)
grid()
legend(loc="lower right")
xlabel("t/T")
ylabel("s")

					  
fig1fig1.pdf

Lorsque m=1 il y a une racine double :

m=1
a=np.array([1,2*m*T,T**2],dtype=np.complex128)
r = racines(a,1e-6)
					  
print(r)
--> [np.complex128(-1+0j), np.complex128(-1+0j)]

Dans cet exemple, il y a effectivement deux racines égales. Lorsque deux racines en théorie égales ne le sont pas à cause des erreurs d'arrondis, l'expression (16) reste valable. Pour traiter le cas où des racines doubles seraient effectivement obtenues par la méthode de Laguerre, nous devons tout d'abord repérer les racines multiples dans la liste des racines. La fonction suivante permet d'obtenir les racines distinctes et leur multiplicité :

def racinesMultiples(r):
	n = len(r)
	rac = []
	mul =[]
	for i in range(n):
		if r[i] in rac:
			j = rac.index(r[i])
			mul[j] += 1
		else:
			rac.append(r[i])
			mul.append(1)
	return rac,mul
			
						
rac,mul = racinesMultiples(r)
						
print(rac)
--> [np.complex128(-1+0j)]
print(mul)
--> [2]

Soit q le nombre de pôles distincts et p1,p2,pq ces pôles. Pour un pôle pk de multiplicité mk , les m solutions linéairement indépendantes qu'il faut considérer sont [2]:

epkt,tepkt,tm-1epkt(18)

Supposons par exemple que le pôle p1 soit double et tous les autres simples. La réponse impulsionnelle s'écrit :

s(t)=b0Ea0+K1ep1t+K2tep2t+K3ep3 tKnepnt(19)

avec p1=p2 . On suppose que les n pôles sont regroupés par pôles égaux (les pôles égaux sont d'indices successifs).

La dérivée s'écrit :

ds(t)dt=K1p1ep1t+K2ep2t+K2tp2ep2t+K3ep3 tKnepnt(20)

et la dérivée seconde :

d2s(t)dt2=K1p12ep1t+2K2p2ep2t+K2tp22ep2t+K3ep3 tKnepnt(21)

Le système linéaire vérifié par les constantes est :

K1+K3+Kn=-b0Ea0p1K1+K2+p2K3pnKn=0p12K1+2p22K2+p32K1+pn2Kn=0p13K1+3p22K2+p33K3+pn3Kn=0p1n-1K1+(n-1)p2n-2K2+p3n-1K3+pnn-1Kn=0

Pour que l'algorithme soit général, il faudrait traiter le cas général d'un pôle de multiplicité m, ce qui complique notablement la détermination du système linéaire pour les constantes.

Une autre approche, beaucoup plus simple à mettre en œuvre, consiste à supposer que si des pôles sont égaux, il suffit de les rendre très légèrement différents pour se ramener au cas de pôles tous distincts. Nous admettons que la solution s(t) évolue continûment lorsqu'on fait varier continûment la valeur d'un pôle (à confirmer). La fonction suivante calcule la réponse impulsionnelle avec cette méthode. Le paramètre eps, multiplié par le module de la racine, définit l'incrément ajouté à chaque racine multiple pour la différentier de la précédente.

def reponseIndicielleBis(a,t,tol=1e-3,eps=1e-8):
	r = racines2(a,tol)
	rac,mul = racinesMultiples(r)
	r = []
	for i in range(len(rac)):
		rho = np.absolute(rac[i])
		for k in range(mul[i]):
			r.append(rac[i]+k*eps*rho)
	n = len(r)
	A = np.zeros((n,n),dtype=np.complex128)
	for j in range(n):
		for i in range(n):
			A[i][j] = r[j]**i
	B = np.zeros(n)
	B[0] = -1
	K = solve(A,B)
	s = 1
	for i in range(n):
		s += K[i]*np.exp(r[i]*t)
	return np.real(s)
					
T = 1
t = np.linspace(0,20*T,1000)
figure()
for m in [0.5,0.8,1,2]:
	a=np.array([1,2*m*T,T**2],dtype=np.complex128)
	s = reponseIndicielleBis(a,t,eps=1e-3)
	plot(t,s,label="m=%0.2f"%m)
grid()
legend(loc="lower right")
xlabel("t/T")
ylabel("s")

					  
fig2fig2.pdf

Voici un exemple avec un pôle de multiplicité 3 :

x0 = -1+1j
x1 = -1+1j
x2 = -1+1j
a = polynome(x0,x1,x2)
r = racines2(a)
					  
print(r)
--> [np.complex128(-1+1j), np.complex128(-1+1j), np.complex128(-1+1j)]

Les pôles déterminés numériquement sont bien égaux. Voici la réponse impulsionnelle :

figure()
t = np.linspace(0,10,1000)
s = reponseIndicielleBis(a,t,eps=1e-3)
plot(t,s)
grid()
xlabel("t")
ylabel("s")
					  
fig3fig3.pdf

Cette méthode semple bien fonctionner mais il faut éviter de choisir une valeur de eps trop petite :

figure()
t = np.linspace(0,10,1000)
s = reponseIndicielleBis(a,t,eps=1e-8)
plot(t,s)
grid()
xlabel("t")
ylabel("s")
					  
fig4fig4.pdf

La simulation Fonction de transfert utilise l'algorithme présenté ici pour tracer la réponse indicielle d'une fonction de transfert jusqu'à l'ordre 4.

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)
[2]  J.P. Demailly,  Analyse numérique et équations différentielles,  (P.U.G., 1991)
Creative Commons LicenseTextes et figures sont mis à disposition sous contrat Creative Commons.