Corrigé TD 7 à 10

Les entiers et les réels

In [57]:
eps=1.0
while not (1.0+0.5*eps) == 1.0: 
    eps *= 0.5
print (eps, 2**(-52))
2.220446049250313e-16 2.220446049250313e-16

Rappelons que la partie fractionnaire d'un réel est codée en base 2 selon $\displaystyle \sum_{i=1}^{M} a_i 2^{-i}$ avec $a_i=0$ ou 1. Dans les lignes ci-dessus, eps commence à 1 et est divisé par deux à chaque itération de la boucle. Il va donc parcourir les fractions $2^{-i}$. Il est important dans le test de la boucle while de comparer 1.0+0.5eps à 1.0 et non 0.5eps à 0 pour que l'exposant ne joue pas (voir ci-après). La valeur de eps lorsque le test s'arrête est telle que $1.0+0.5*eps \equiv\underbrace{1.0000000000000_2}_{M bits} = 1.0$. Pour la valeur précédente, il correspond à une représentation pour laquelle on a en gros le dernier bit de la mantisse qui est non nul, soit environ $\underbrace{1.0000000000001_2}_{M bits}$ mais il peut être plus grand. D'après le site baseconvert, il semble que ce soit plutôt $\underbrace{1.00000000000011_2}_{M bits}$. On obtient donc une bonne estimation de la précision machine, qui de l'ordre de $2^{-M}$ avec ici $M=52$ sur une machine 64bits qui correspond à l'erreur d'arrondi entrainé par la troncation de la partie fractionnaire sur un nombre de bits fini. Sur le site baseconvert.com, on peut voir que 1.000000000000001 est le dernier décimal dont la représentation binaire sur 64bits est différente de 1.

In [58]:
eps=1.0
while not (0.0+0.5*eps) == 0.0: 
    eps *= 0.5
print (eps, 2**(-1022), 2**(-1022-52))
5e-324 2.2250738585072014e-308 5e-324

Dans ce cas, l'exposant n'est plus 0 dans le test et eps peut descendre à des valeurs beaucoup plus petites au fur et à mesure que l'on divise par deux. L'exposant le plus petit que l'on peut faire est -1022. En effet, l'exposant est codé sur 11bits avec un décalage de sorte que $E=0\equiv -1022$, $E=1022\equiv 0$ et $E=2047=2^{11}-1\equiv +1023$. L'exposant le plus petit possible est donc -1022 et le plus grand 1023. Cela correspond à l'ordre de grandeur des nombres les plus grands dont la mantisse est de l'ordre de 1 (c'est l'ordre de grandeur donné dans le cours $2^{-1022}\simeq 2.22507385851\times 10^{-308}$). Maintenant, la mantisse peut représenter un nombre très petit, de l'ordre de $2^{-52}$ si tout les bits sont mis à 0 sauf le dernier. Au total, le nombre le plus petit accessible est donc de l'ordre de $2^{-1022-52} \simeq 4.94065645841\times 10^{-324}$ qui est bien celui affiché par le code.

In [59]:
0.125+0.25
Out[59]:
0.375
In [60]:
0.1+0.2
Out[60]:
0.30000000000000004

Cela est surprenant, on s'attendrait à ce que l'ordinateur pour calculer exactement, sans erreur d'arrondi cette addition toute simple.

In [61]:
from decimal import Decimal

print( Decimal("0.1") ) # affiche la représentation décimale à partir d'une string
print( Decimal(0.1) )# affiche la représentation décimale du réel 0.1 codé sur un mot mémoire
print( Decimal(0.1+0.2), Decimal(0.3) )

print( Decimal("0.1")+Decimal("0.2") )
print( Decimal(0.25), Decimal(0.125) )
print( Decimal(0.35-0.1) )
0.1
0.1000000000000000055511151231257827021181583404541015625
0.3000000000000000444089209850062616169452667236328125 0.299999999999999988897769753748434595763683319091796875
0.3
0.25 0.125
0.2499999999999999722444243843710864894092082977294921875

On voit que 0.1 comme float représenté sur la machine ne correspond en fait pas à la valeur attendue 0.1 en base décimale mais l'approximation 0.1000000000000000055511151231257827021181583404541015625... Cela est dû au fait que 0.1 a une représentation infinie en base 2 de la même manière que 1/3 a une représentation infinie en base 10 (base décimale) que l'on écrit souvent 0.33333333333.... Le site baseconvert nous permet de faire afficher le début tronqué de la représentation en binaire de 0.1: il s'agit de: 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 101. Du coup, dans les opérations suivantes, cette inexactitude de représentation due à la troncation se propage (un peu heureusement mais pas trop), et le résultat 0.1+0.2 n'est pas représenté par la représentation correspondant à 0.3 d'où l'erreur d'arrondi apparaissant dans l'affichage. En revanche, pour $0.25=2^{-2}$ et $0.125=2^{-3}$ qui sont représentable exactement, le résultat de leur addition est aussi exact. Par contre, pour $0.35-0.1$, bien que le résultat soit représentable exactement, les intermédiaires de calcul ne le sont pas et le résultat ne tombe pas sur celui attendu.

In [62]:
import numpy as np
print( np.uint8(1000), 1000%256 )
print( np.uint8(230)+np.uint8(120) )
print( np.uint8(-20), np.uint8(3.4*41) )
232 232
94
236 139
C:\Users\guillaume\Anaconda3\lib\site-packages\ipykernel\__main__.py:3: RuntimeWarning: overflow encountered in ubyte_scalars
  app.launch_new_instance()

On observe qu'il faudra se méfier avec les conversions modulo 256 des valeurs intermédiaires ainsi que celles qui ont des résultats intermédiaires négatifs. La valeur maximale 256 est assez vite atteinte et peut conduire à un overflow.

Algèbre linéaire avec Numpy

In [63]:
import numpy as np
np.set_printoptions(precision = 4, suppress=True)
L = 10
In [64]:
x = np.arange(1,L+1)
V = np.sqrt(2/(L+1))*np.sin(np.pi*x/(L+1))
print( V, np.linalg.norm(V))
[ 0.1201  0.2305  0.3223  0.3879  0.4221  0.4221  0.3879  0.3223  0.2305
  0.1201] 1.0
In [65]:
W = np.diag([-2.]*L,k=0) + np.diag([1.]*(L-1),k=1) + np.diag([1.]*(L-1),k=-1)
print (W)
[[-2.  1.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 1. -2.  1.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  1. -2.  1.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  1. -2.  1.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  1. -2.  1.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  1. -2.  1.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  1. -2.  1.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  1. -2.  1.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  1. -2.  1.]
 [ 0.  0.  0.  0.  0.  0.  0.  0.  1. -2.]]
In [66]:
A = np.dot(W,V)
print (A / V)
[-0.081 -0.081 -0.081 -0.081 -0.081 -0.081 -0.081 -0.081 -0.081 -0.081]
In [67]:
(val,U) = np.linalg.eigh(W)
print( "valeurs propres:", val)
print( "vecteurs propres:")
for i in range(L): print( i,":",U[:,i])
valeurs propres: [-3.919  -3.6825 -3.3097 -2.8308 -2.2846 -1.7154 -1.1692 -0.6903 -0.3175
 -0.081 ]
vecteurs propres:
0 : [-0.1201  0.2305 -0.3223  0.3879 -0.4221  0.4221 -0.3879  0.3223 -0.2305
  0.1201]
1 : [-0.2305  0.3879 -0.4221  0.3223 -0.1201 -0.1201  0.3223 -0.4221  0.3879
 -0.2305]
2 : [-0.3223  0.4221 -0.2305 -0.1201  0.3879 -0.3879  0.1201  0.2305 -0.4221
  0.3223]
3 : [-0.3879  0.3223  0.1201 -0.4221  0.2305  0.2305 -0.4221  0.1201  0.3223
 -0.3879]
4 : [ 0.4221 -0.1201 -0.3879  0.2305  0.3223 -0.3223 -0.2305  0.3879  0.1201
 -0.4221]
5 : [ 0.4221  0.1201 -0.3879 -0.2305  0.3223  0.3223 -0.2305 -0.3879  0.1201
  0.4221]
6 : [ 0.3879  0.3223 -0.1201 -0.4221 -0.2305  0.2305  0.4221  0.1201 -0.3223
 -0.3879]
7 : [ 0.3223  0.4221  0.2305 -0.1201 -0.3879 -0.3879 -0.1201  0.2305  0.4221
  0.3223]
8 : [-0.2305 -0.3879 -0.4221 -0.3223 -0.1201  0.1201  0.3223  0.4221  0.3879
  0.2305]
9 : [-0.1201 -0.2305 -0.3223 -0.3879 -0.4221 -0.4221 -0.3879 -0.3223 -0.2305
 -0.1201]
In [68]:
Um1 = np.linalg.inv(U)
D = np.diag(val)
print( np.dot(U, np.dot(D,Um1)))
print( np.dot(Um1, np.dot(W,U)))
[[-2.  1. -0.  0. -0. -0.  0.  0.  0. -0.]
 [ 1. -2.  1.  0.  0.  0. -0. -0. -0.  0.]
 [-0.  1. -2.  1.  0. -0. -0.  0. -0.  0.]
 [ 0.  0.  1. -2.  1.  0. -0.  0. -0. -0.]
 [-0.  0.  0.  1. -2.  1. -0. -0.  0.  0.]
 [-0. -0. -0. -0.  1. -2.  1.  0.  0. -0.]
 [ 0.  0.  0. -0.  0.  1. -2.  1.  0.  0.]
 [-0.  0.  0.  0. -0. -0.  1. -2.  1.  0.]
 [ 0.  0. -0. -0.  0.  0.  0.  1. -2.  1.]
 [ 0.  0. -0.  0. -0. -0.  0. -0.  1. -2.]]
[[-3.919   0.     -0.     -0.     -0.      0.      0.      0.     -0.     -0.    ]
 [ 0.     -3.6825 -0.      0.      0.     -0.     -0.      0.     -0.      0.    ]
 [-0.      0.     -3.3097 -0.      0.      0.      0.     -0.     -0.     -0.    ]
 [-0.     -0.      0.     -2.8308 -0.      0.      0.      0.      0.      0.    ]
 [-0.      0.      0.      0.     -2.2846 -0.     -0.     -0.     -0.     -0.    ]
 [ 0.     -0.     -0.      0.     -0.     -1.7154  0.      0.     -0.     -0.    ]
 [ 0.      0.      0.     -0.     -0.     -0.     -1.1692  0.      0.     -0.    ]
 [ 0.     -0.      0.     -0.     -0.      0.      0.     -0.6903  0.      0.    ]
 [-0.     -0.      0.      0.      0.      0.      0.      0.     -0.3175
  -0.    ]
 [-0.     -0.      0.      0.     -0.     -0.      0.      0.      0.
  -0.081 ]]

Application à la dynamique d'une chaîne d'oscillateurs couplés

In [69]:
Omega = np.sqrt(-val); print( Omega)
[ 1.9796  1.919   1.8193  1.6825  1.5115  1.3097  1.0813  0.8308  0.5635
  0.2846]

Conditions initiales $Y^0 = U^{-1}X^0$

In [70]:
X0 = np.zeros( (L,) ); X0[0] = 0.2; X0[1] = 0.1; print( X0)
# en partant d'un état propre
# X0 = U[:,1]
[ 0.2  0.1  0.   0.   0.   0.   0.   0.   0.   0. ]
In [71]:
Y0 = np.dot(Um1,X0); print( Y0)
[-0.001  -0.0073 -0.0222 -0.0453  0.0724  0.0964  0.1098  0.1067 -0.0849
 -0.0471]

Solution des équations: $X(t) = UY(t)$ avec $Y_j(t) = Y^0_j\cos(\omega_j t)$

In [72]:
t = 10
X = np.dot(U,Y0*np.cos(Omega*t))
print (X)
[-0.0033  0.003   0.0062 -0.0233  0.0181  0.0446 -0.0599 -0.1107 -0.0359
  0.0202]
In [73]:
t = np.linspace(0.0,10.0,101)
allX = np.zeros( shape=(L,len(t)) )
for i in range(len(t)):
    allX[:,i] = np.dot(U,Y0*np.cos(Omega*t[i]))
In [74]:
from pylab import *
%matplotlib inline

for x in range(L):
    plot(t, allX[x,:] )

xlabel('temps')
ylabel('Position')
title("Dynamique d'oscillateurs couples")
grid(True)
show()

Manipuler des images

Chargement et sauvegarde

In [75]:
import matplotlib.pyplot as pl
import matplotlib.image as mpim
%matplotlib inline

img = mpim.imread("grenouille.jpg")
type(img)
print( img.shape )
(250, 400, 3)

L'image jpeg est importée sous la forme d'un tableau type ndarray de numpy qui a 3 dimensions.

In [76]:
pl.imshow(img)
Out[76]:
<matplotlib.image.AxesImage at 0xad71860>

On voir que la première dimension est la dimension verticale (lignes) et la seconde la dimension horizontale (colonnes), comme une matrice $N\times M$. Selon la numérotation des axes qui correspondent aux indices, les pixels sont numéroter de haut en bas et de gauche à droite avec donc le premier (indice 0,0) en haut à gauche et le dernier (indice $N-1,M-1$ en bas à droite.

In [77]:
pl.axis('off')
pl.imshow(img)
Out[77]:
<matplotlib.image.AxesImage at 0xa941b38>
In [78]:
print( img[100,100,:])
[41 78 27]

Les deux premières dimensions correspondent à la position du pixel et la troisième au codage RVB de la couleur.

In [79]:
imgcopy = np.copy(img)
mpim.imsave("grenouille_copy.jpg",imgcopy)

On a $N\times M$ pixels contenant chacun 3 octets pour les couleurs RVB, on s'attend donc à une taille en Ko de

In [80]:
print( 250*400*3/1024,"Ko")
292.96875 Ko

La taille est plus petite car le format jpeg est compressé.

Agir sur la structure spatiale

In [81]:
pl.imshow(img[::,::-1]) # réflection par rapport à la verticale
Out[81]:
<matplotlib.image.AxesImage at 0xa9f1550>
In [82]:
pl.imshow(img[::-1,::]) # réflection par rapport à l'horizontale
Out[82]:
<matplotlib.image.AxesImage at 0xaa52710>
In [83]:
pl.imshow(img[::-1,::-1]) # rotation de 180°
Out[83]:
<matplotlib.image.AxesImage at 0xaab5860>
In [84]:
pl.imshow(img[::2,::2]) # réduction de la taille par deux en sélectionnant les indices pairs
Out[84]:
<matplotlib.image.AxesImage at 0xd4d7cc0>
In [85]:
pl.imshow(img[:img.shape[0]//2:,:img.shape[1]//2:]) # extraction du quadrant supérieur gauche
Out[85]:
<matplotlib.image.AxesImage at 0xd53cb00>
In [86]:
nl, nc, s = img.shape # on récupère les dimensions dans nl et nc, s=3
rayon = 120           # rayon voulu en pixel
Y, X = np.ogrid[:nl,:nc] # crée le tableau 2D des indices de la photo
masque = (X-nc/2)**2 + (Y-nl/2)**2 > rayon**2 # définition de l'extérieur du cercle
                                              # centré sur le milieu de l'image
print( masque[0,0] ) # le masque est une matrice de booléen
pl.imshow(masque, cmap=pl.cm.gray) # on affiche le masque, cmap est la palette de couleur
True
Out[86]:
<matplotlib.image.AxesImage at 0xd59e400>
In [87]:
img_coupee = np.copy(img)
pixel_noir = np.array([0,0,0],dtype=np.uint8) # définition du pixel noir
img_coupee[masque] = pixel_noir          # les indices du masque renvoyant True sont mis en noir
pl.imshow(img_coupee)
Out[87]:
<matplotlib.image.AxesImage at 0xae4e780>
In [88]:
a,b = 150.,100.     # demi grand axe et demi petit axe
masque = ((X-nc/2)/a)**2 + ((Y-nl/2)/b)**2 > 1 # définition de l'extérieur de l'ellipse
                                              # centrée sur le milieu de l'image
pl.imshow(masque, cmap=pl.cm.gray) # on affiche le masque, cmap est la palette de couleur
Out[88]:
<matplotlib.image.AxesImage at 0xa971ba8>
In [89]:
img_coupee = np.copy(img)
pixel_noir = np.array([255,255,255],dtype=np.uint8) # définition du pixel blanc
img_coupee[masque] = pixel_noir          # les indices du masque renvoyant True sont mis en blanc
pl.imshow(img_coupee)
Out[89]:
<matplotlib.image.AxesImage at 0xae72d68>

Agir sur les couleurs

In [90]:
neg = 255*np.ones(img.shape,dtype=np.uint8)
neg -= img
pl.imshow(neg)
Out[90]:
<matplotlib.image.AxesImage at 0xe5ed0f0>

La première ligne crée une image blanche de même taille que la photo. La seconde soustrait les couleurs de l'image au blanc et renvoie donc la couleur complémentaire.

In [91]:
def linear(source,v):
    res = np.zeros(source.shape,dtype=np.uint8)
    ny, nx, s = source.shape
    for i in range(ny):
        for j in range(nx):
            res[i,j] = np.array(source[i,j]*v,dtype=np.uint8)
    return res
In [92]:
rouge = linear(img,np.array([1,0,0]))
vert  = linear(img,np.array([0,1,0]))
bleu  = linear(img,np.array([0,0,1]))
In [93]:
pl.subplot(1, 3, 1); pl.axis('off'); pl.imshow(rouge)
pl.subplot(1, 3, 2); pl.axis('off'); pl.imshow(vert)
pl.subplot(1, 3, 3); pl.axis('off'); pl.imshow(bleu)
pl.show()
pl.axis('off'); pl.imshow(rouge+vert+bleu)
Out[93]:
<matplotlib.image.AxesImage at 0xe724d68>
In [94]:
blanc = 255*np.ones(img.shape,dtype=np.uint8)
magenta = blanc - vert
pl.axis('off'); pl.imshow(magenta)
Out[94]:
<matplotlib.image.AxesImage at 0xae4e3c8>
In [95]:
# réécriture en une ligne de la fonction linear
def linear(source,v):
    return np.array(source[:,:]*v,dtype=np.uint8)
In [96]:
def contraste(source,f):
    return np.array(f(source[:,:]),dtype=np.uint8)
In [97]:
s = 5.0
a, b = np.tanh(s*(-127.5)/255.), np.tanh(s*(255.-127.5)/255.)
f = lambda x: 255*(np.tanh(s*(x-127.5)/255.)-a)/(b-a)
In [98]:
x = np.arange(256)
print( np.max(f(x)), np.min(f(x)), f(127.5))
pl.plot(x,f(x))
255.0 0.0 127.5
Out[98]:
[<matplotlib.lines.Line2D at 0xe80f8d0>]
In [99]:
pl.imshow(contraste(img,f))
Out[99]:
<matplotlib.image.AxesImage at 0xe86ccf8>

Compression par décomposition en valeurs singulières

construction de la matrice en niveau de gris.

In [100]:
def rgb2gray(rgb):
    return np.dot(rgb, [0.299, 0.587, 0.144])
A = rgb2gray(img) # matrice en niveau de gris
pl.imshow(A, cmap=pl.cm.gray)
pl.clim(0,255)
In [101]:
U, s, V = np.linalg.svd(A)
pl.semilogy()
pl.plot(s/np.max(s))
Out[101]:
[<matplotlib.lines.Line2D at 0xf8c0208>]
In [102]:
kept = 25
S = np.diag(s[:kept])
R = np.dot(U[:,:kept],np.dot(S,V[:kept,:]))
pl.subplot(1, 2, 1)
pl.imshow(R,cmap=pl.cm.gray)
pl.clim(0,255); pl.axis('off')
pl.subplot(1, 2, 2)
pl.imshow(A,cmap=pl.cm.gray)
pl.clim(0,255); pl.axis('off')
Out[102]:
(-0.5, 399.5, 249.5, -0.5)
In [103]:
taille_orig = A.size
taille_comp = U[:,:kept].size+S.size+V[:kept,:].size
print( "Taux de compression:",taille_orig/float(taille_comp))
Taux de compression: 5.925925925925926
In [104]:
mpim.imsave("PetiteGrenouille.jpg",R,cmap=pl.cm.gray)
In [105]:
n, bins, patches = pl.hist(A.flatten(), 256, range=(0.0,255), fc='k', ec='k')