Table des matières PDFPython

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 = 1
a = polynome(x0,x1,x2)
r = racines2(a,tol=1e-6)
				 
print(r)
--> [np.complex128(1+0j), np.complex128(1+0j), np.complex128(1-0j)]
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.