# Ceci est un commentaire pour l'import des packages.
import numpy as np # package de calcul scientifique
import matplotlib.pyplot as plt # package graphique
Prise en main de Python pour l’optimisation
Préambule
Pour ce TP (ainsi que les suivants), on privilégiera le logiciel Vscodium sur les machines de l’université (ou VSCode au choix). On utilisera l’aide de base de python
avec les commandes help(la-fonction)
ou ?la-fonction
. L’aide en ligne est aussi conseillée, surtout pour la création de graphiques avec matplotlib
pour avoir plus de détails et des galleries de visualisation.
Dans ce fichier, vous trouverez du code (à copier-coller / adapter / compléter) et les questions à faire. Les endroits à compléter sont indiqués avec les symboles ...
ou XXX TODO
.
Introduction à numpy
Dans un premier temps, importons les packages nécessaires de la manière classique suivante:
Un nombre dans un ordinateur c’est quoi ?
Un nombre en python
peut être un entier (🇬🇧: int) ou un flottant (🇬🇧: float). Ce sont les deux types les plus populaires, mais il en existe d’autres.
= 1
entier print(type(entier))
= 2.5
flottant print(type(flottant))
<class 'int'>
<class 'float'>
Question : Dépassement (🇬🇧: overflow)
On cherche à savoir quelles sont les limites de la machine pour représenter un nombre flottant. x_{max} obtenu avec la commande np.finfo
1.
= ... x_max
Tester l’égalité entre x_{max} et x_{max}+1. Faire de même entre x_{max} et \eta x_{max} avec \eta\simeq 1, \eta > 1. On rappelle comment faire les dests simples ci-dessous:
= 1
a = 2
b print(a==b) # égalité
print(a!=b) # différence
print(a > b) # plus grand
print(a <= b) # plus petit ou égal
False
True
False
True
print(...)
= ...
eta print(...)
Quel est le résultat de l’opération 1.1\times x_{max} - 1.1 \times x_{max} ? Comment l’expliquez-vous ?
Pour tester l’égalité entre flottants avec numpy
on utilise plutôt:
+ 1, xmax) np.testing.assert_almost_equal(xmax
Un test ne renvoie rien s’il est vérifié et continue à executer les opérations suivantes. Si le test n’est pas vérifié, une erreur est renvoyée et arrête l’execution du code.
On parle de dépassement (🇬🇧: overflow) quand un nombre dépasse le plus grand flottant que l’ordinateur peut représenter en mémoire. Notez que les problèmes de dépassement peuvent coûter très très cher, même à d’excellents scientifiques2.
Question : Soupassement (🇬🇧: underflow)
Le soupassement est le phénomène inverse du dépassement. Cela se produit quand un nombre est tellement proche de 0 que l’ordinateur ne le différencie plus de 0. Observez un cas d’underflow (par exemple trouver à partir de quelle puissance négative de 2 ce phénomène intervient)
La commande range(a,b)
génère des nombres entre a et b-1 avec un pas de 1. Par défaut si a n’est pas spécifié il est fixé à 0.
print(list(range(6)))
for val in range(6): # pas besoin de list si on itère
print(val) # val prend les valeurs générées
[0, 1, 2, 3, 4, 5]
0
1
2
3
4
5
Cependant, une grille linéaire n’est pas forcément adaptée quand on manipule des valeurs d’ordres de grandeur très différents. On préfère alors une grille géométrique, par exemple avec np.logspace
.
Trouver le plus petit nombre flottant indistinguable de 0 en numpy
dans le vecteur n_petits
:
= 1200
n_petit = np.logspace(-n_petit, 0, base=2, num=n_petit + 1)
n_petits print(n_petits)
print(n_petits[::-1])
for idx, val in enumerate(n_petits[::-1]): # faire une boucle
print(idx, val)
if val <= ...: # XXX TODO
break # on arrête la boucle
print(idx, val)
Question : Arrondis
Regardons comment les arrondis sont effectués sur des flottants:
print(0.6, 0.3 + 0.2 + 0.1)
print(0.6, 0.1 + 0.2 + 0.3)
0.6 0.6
0.6 0.6000000000000001
Question : Précision relative et absolue
Pour éviter des erreurs d’arrondis, on peut contrôler l’égalité d’un point de vue numérique en acceptant une certaine marge d’erreur:
a\simeq b \Longleftrightarrow |a-b| \leq \text{atol} + \text{rtol}|b|
Utilisez la fonction isclose
de numpy
en choisissant des paramètres atol
, rtol
pour que l’égalité dans la question précédente soit vraie pour un choix, et fausse pour un autre choix de paramètres.
help(np.isclose)
0.6, 0.1 + 0.2 + 0.3, atol=..., rtol=...) # XXX TODO np.isclose(
On pourra aussi utiliser la fonction allclose
, qui fonctionne de manière similaire pour des tableaux.
Opérations matricielles avec Python
Pour cette partie, un rappel visuel des créations/transformations usuelles de matrices est disponible ici
Question : opérations élémentaires
Manipulez les opérations classiques sur des matrices (arrays) de numpy
(si vous êtes déjà habitués à numpy
vous pouvez continuer)
Opérations termes à termes:
# Somme de deux vecteurs
= np.array([1.0, 2, 3])
A = np.array([-1, -2, -3.0])
B
# Attribuer à la variable C la somme de A et B
= ... # XXX TODO
sum_A_B
3,)), sum_A_B)
np.testing.assert_almost_equal(np.zeros((print("it worked")
# Le produit terme à terme avec *
= ... # XXX TODO
prod_A_B
-1.0, -4, -9]), prod_A_B)
np.testing.assert_almost_equal(np.array([print("it worked")
# Remarque: la même chose fonctionne terme à terme avec \, ** (puissance)
1.0, 4, 9]), A ** 2)
np.testing.assert_almost_equal(np.array([print("it worked: even for powers")
Le produit scalaire (ou matriciel) est l’opérateur @
. Vérifiez que pour la matrice J ci-dessous J^3 = Id de deux façons. Pour cela on pourra aussi utiliser la puissance matricielle avec np.linalg.matrix_power
:
= np.array([[0, 0, 1.0], [1.0, 0, 0], [0, 1.0, 0]])
J
= np.eye(3)
I3
# XXX TODO
np.testing.assert_almost_equal(I3, ...) print("it worked: method 1")
# XXX TODO
np.testing.assert_almost_equal(I3, ...) print("it worked: method 2")
Question : résolution de systèmes linéaires
Pour résoudre le système Ax=b en mathématiques, la formule explicite est x=A^{-1}b (dans le cas où A est inversible).
print(f"L'inverse de la matrice: \n {J} \n est \n {np.linalg.inv(J)}")
= 20 # XXX TODO: tester avec n=100
n = np.roll(np.eye(n), -1, axis=1) # matrice de permutation de taille n
Jbig print(Jbig)
= np.arange(n)
b print(b)
# on peut transposer une matrice facilement de 2 manières:
print(Jbig)
print(Jbig.T)
print(np.transpose(Jbig))
Comparons niveau temps d’execution l’inversion explicite vs. l’utilisation d’un solveur de système linéaire tel que np.linalg.solve
:
import time
# Résolution de système par une méthode naive: inversion de matrice
= time.perf_counter() # XXX TODO
t0 = ... @ b
y1 = ...
timing_naive print(
f"Temps pour résoudre un système avec la formule mathématique: {timing_naive:.4f} s."
)
# Résolution de système par une méthode adaptée: fonctions dédiée de `numpy``
# XXX TODO
= ...
y2 = ...
timinig_optimized print(
f"Temps pour résoudre un système avec la formule mathématique: {timing_optimized:.4f} s.\nC'est donc {timing_naive / timing_optimized} fois plus rapide d'utiliser la seconde formulation"
)
np.testing.assert_almost_equal(y1, y2)print("Les deux méthodes trouvent le même résultat")
Question découpage (🇬🇧: slicing)
Le découpage permet d’extraire des éléments selon un critère (position, condition, etc.). La notation :
signifie “tout le monde”, et l’indexation commence en 0. Pour partir de la fin, il est possible de mettre le signe -
devant le nombre: ainsi -1 renvoie donc au dernier élément.
print(f"The first column is {A[:, 0]}")
# Afficher la deuxième ligne de A
print(f"The second row is {...}") # XXX TODO
Mettre à zéro une ligne sur 2 de la matrice identité de taille 5\times 5
= np.eye(5, 5)
C = 0 # mettre à zéro une ligne sur deux. # XXX TODO C[...,...]
Question : grilles
Il existe plusieurs types de grilles de nombres, les linéaires et les géométriques sont les plus courantes. La fonction linspace
du package numpy
permet de générer une grille de points entre deux valeurs de manière linéaire. Sa syntaxe est np.linspace(val1, val2, num=<nombre de points>)
Associer à x
une grille de 100 points entre -5 et 5. Créer un vecteur contenant toutes les puissances de 10 de l’exposant 1 à l’exposant 9.
# grille linéaire
= ... # XXX TODO
x print(x)
# Grille géométrique:
=9) # XXX TODO np.logspace(..., ..., num
Question : dimensions et objets
Connaître les dimensions de l’objet que l’on utilise est plus nécessaire ! La dimension est un attribut nommé shape
associé à l’array
. Pour accéder à un attribut d’un objet, on utilise la syntaxe objet.<nom_attribut>
Dans la cellule suivante, afficher la taille de d
. Quelles sont les dimensions de d.reshape(2,3)
? d.reshape(3,2)
? d.reshape(6,)
? d.reshape(1,6)
? Commentez
= np.arange(6)
d # taille de d ?
# XXX TODO
numpy
utilise pour les vecteurs des tableaux n’ayant pas de deuxième dimension (car dans la mémoire de l’ordinateur, la seconde dimension n’existe pas: il y a donc une simple instruction qui dit si l’on parcourt le tableau en ligne ou en colonne). C’est une différence majeure avec matlab
et R
par exemple.s
Introduction à matplotlib
Visualisation graphique
Question : Affichage de fonctions 1D
Le but ici est de tracer les graphes de fonctions avec matplotlib
et de mettre en valeurs plusieurs points intéressants.
- Tracer la fonction f:x\mapsto \cos(x) sur l’intervalle [-10,10].
- Utiliser la fonction
hlines
dematploltib
pour tracer les droites y\pm 1 (on pourra modifierlinestyles
pour faire un trait pointillé). - Utiliser la fonction
scatter
pour mettre en valeur ses points critiques (modifier l’options
pour la taille des marqueurs) - Tout graphique doit contenir un titre, être légendé et être lisible ! Modifiez la taille de l’image avec l’argument
figsize
et rendez ce graphique présentable en modifiant le paramètrezorder
pour faire en sorte que les points apparaissent au dessus de la courbe.
# XXX TODO
= np.linspace(...)
x = np.cos(...)
y
=(8, 6))
plt.figure(figsize=r"$f:x\mapsto \cos(x)$", zorder=1)
plt.plot(x, y, label
plt.hlines(=...,
y=...,
xmin=...,
xmax=r"$\pm 1$",
label="r",
color=1,
zorder="dotted",
linestyles
)
plt.hlines(...)
= np.pi * np.arange(-3, 4)
x_extrema = np.cos(x_extrema)
y_extrema
=...)
plt.scatter(..., s
plt.xlabel(...)
plt.ylabel(...)
plt.legend()
plt.title(...)
plt.tight_layout() plt.show()
Question : graphes multiples
En utilisant subplot
, affichez une double figure représentant les fonctions f:x\mapsto \exp(-\lambda x) sur l’intervalle [0, 10] pour \lambda=1,\dots,5. Le premier graphe (en haut) sera en échelle habituelle et le second (en bas) sera en échelle semi-logarithmique sur l’axe des y pour mieux visualiser les convergences vers 0. On choisira des couleurs différentes pour chaque valeur de \lambda Ajouter des titres et sous-titres.
# Lise de couleurs, dégradé de violet:
= plt.cm.Purples(np.linspace(0.3, 1, 5))
colors = np.arange(1, 6)
lambdas = np.linspace(0, 10, 1000, endpoint=True)
x
= plt.subplots(2, 1, sharex=True, figsize=(6, 8))
fig, axs
for i in range(...):
= np.exp(-x * ...)
y 0].plot(x, y, color=colors[i]) # XXX TODO
axs[1].semilogy(x, y, color=colors[i], label=f'$\lambda_{{{i}}}$')
axs[
# Titres / sous-titres
"Décroissance exponentielle")
fig.suptitle(0].set_title("Échelle classique")
axs[1].set_title("Échelle semi-logarithmique")
axs[1].legend(loc=3) axs[
Fonctions à plusieurs variables
On va créer ici une grille de maillage (🇬🇧: meshgrid) avec la commande np.meshgrid
. Cette fonction permet principalement de créer les points sur lesquels on va évaluer une fonction de deux variables pour afficher un graphique en 3D:
Code
# Créer une grille de points avec meshgrid: exemple
= np.linspace(-10, 10, 11)
x = np.linspace(0, 20, 11)
y = np.meshgrid(x, y)
xx, yy
# xx est x répété "le nombre de points dans y" fois sur les lignes
# yy est y répété "le nombre de points dans x" fois sur les colonnes
= plt.subplots(nrows=1, ncols=2, figsize=(7, 3))
fig_level_set, axs
0].plot(xx, yy, ls="None", marker=".")
axs[0].set_aspect('equal')
axs[1].plot(yy, xx, ls="None", marker=".")
axs[1].set_aspect('equal')
axs[
plt.show()
# Une fonction à deux variables pourra ainsi être visualisée en l'évaluant
# sur chacun des points d'une grille assez fine.
Rappel du cours: “Pour tout ouvert U de \mathbb{R}^2, si x_0\in U est un minimum local de la fonction f \in \mathcal{C}^2(U), alors f'(x_0)=0 et f''(x_0) est semi-définie positive”
La réciproque de ce théorème est fausse: considérer f_{selle}(x,y)=x^2-y^4 en (0,0).
Question : Conditions du premier et second ordre
Créer alors la surface 3D de la fonction f_{selle} et afficher ses courbes de niveaux. Ajouter le point à l’origine sur les graphiques. On pourra utiliser la fonction plt.scatter
. Interpréter visuellement le comportement de la surface en (0,0).
# Gestion de la colorbar
from pylab import cm # noqa:E402
def f(x, y):
return x ** 2 - y ** 4
= np.arange(-1, 1, 0.05)
x = np.arange(-1, 1, 0.05)
y = np.meshgrid(x, y)
X, Y = f(X, Y)
Z
# Figure : lignes de niveau.
= plt.subplots(1, 1, figsize=(3, 3))
fig_level_sets, ax_level_sets r"$x^2 - y^4$: lignes de niveau")
ax_level_sets.set_title(= ax_level_sets.contourf(X, Y, Z, levels=30, cmap="RdBu_r")
level_sets =ax_level_sets, fraction=0.046, pad=0.04)
fig_level_sets.colorbar(level_sets, ax
# Figure : surface
= plt.subplots(
fig_surface, ax_surface 1, 1, figsize=(3, 3), subplot_kw={"projection": "3d"}
)r"$x^2 - y^4$: surface")
ax_surface.set_title(= ax_surface.plot_surface(
surf
X,
Y,
Z,=1,
rstride=1,
cstride=cm.RdBu_r,
cmap=0,
linewidth=False,
antialiased=0.8,
alpha
)# XXX TODO
=..., zorder=2)
ax_level_sets.scatter(..., alpha
ax_surface.scatter(...) plt.show()
TypeError: scatter() missing 1 required positional argument: 'y'
Question : Dérivées directionnelles
On considère maintenant la fonction f_{dir}(x,y) = \begin{cases} \frac{xy}{x^2+y^2}& \text{si} (x,y)\neq(0,0) \\ 0 &\text{sinon}\end{cases} \enspace. Expliquer pourquoi cette fonction admet des dérivées partielles en tout point. Est-ce que la fonction f_{dir} est continue en tout point (en particulier à l’origine) ? Visualiser ses lignes de niveaux (en afficher 12) et la surface engendrée et les gradients de la fonction. Pour cela, on commence par définir tout ce dont on a besoin:
def z(x, y):
return np.where(np.allclose([x, y], 0), 0, x * y / (x ** 2 + y ** 2))
def dx(x, y):
return np.where(
0),
np.allclose([x, y], 0,
* (y ** 2 - x ** 2) / (x ** 2 + y ** 2) ** 2,
y
)
def dy(x, y):
return np.where(
0),
np.allclose([x, y], 0,
* (x ** 2 - y ** 2) / (x ** 2 + y ** 2) ** 2,
x )
Et seulement ensuite on peut créer les graphiques:
= np.arange(-0.5, 0.5, 0.01)
x = np.arange(-0.5, 0.5, 0.01)
y = np.meshgrid(x, y)
X, Y = z(X, Y)
Z
= dx(X, Y)
dX = dy(X, Y)
dY = np.sqrt(dX * dX + dY * dY)
speed
# Figure 1
= plt.figure(figsize=(8, 4))
fig1 = fig1.add_subplot(1, 2, 1)
ax1 = ax1.contourf(X, Y, Z, levels=30, cmap="RdBu_r") # XXX TODO
im ="k", linewidth=5 * speed / speed.max())
ax1.streamplot(X, Y, dX, dY, colormin(), x.max()])
ax1.set_xlim([x.min(), y.max()])
ax1.set_ylim([y."equal")
ax1.set_aspect(= fig1.colorbar(im, ax=ax1, fraction=0.046, pad=0.04)
cbar r"$\frac{xy}{x^2 + y^2}$: lignes de niveau et gradients")
ax1.set_title(
= fig1.add_subplot(1, 2, 2, projection="3d")
ax2 r"$\frac{xy}{x^2 + y^2}$: surface")
ax2.set_title(= ax2.plot_surface(
surf
X,
Y,
Z,=1,
rstride=1,
cstride=cm.RdBu_r,
cmap=0,
linewidth=False,
antialiased
)
plt.tight_layout() plt.show()
En terme de variante on pourra considérer le graphique ci-desous et investiguer l’interprétation des lignes proposées:
# Figure 2
= plt.subplots(1, 1, figsize=(4, 4))
fig2, ax
ax.imshow(=cm.RdBu_r, origin="lower", extent=[min(x), max(x), min(y), max(y)]
Z, cmap
)= ax.contour(X, Y, Z, colors="white", alpha=1, linewidths=0.8)
CS =True, fontsize=10)
ax.clabel(CS, inline"Alternative")
ax.set_title(=3.0)
plt.tight_layout(pad plt.show()