USMA1Q — Méthodes Numériques¶

Séance 3 — Calcul numérique avec NumPy¶

Conservatoire National des Arts et Métiers

04 Mars 2026

nicholas-anton.collins-craft@enpc.fr

Un binder qui peut faire tourner le notebook qui contient les exercices se trouve ici :

https://mybinder.org/v2/gh/nickcollins-craft/USMA1Q-Methodes-Numeriques/HEAD

Plan du cours¶

Jour Sujet
03 mars Fondamentaux de la programmation en Python
04 mars (matin) Fonctions, listes approfondies, fichiers et graphiques
04 mars (après-midi) Calcul numérique avec NumPy
11 mars Tableaux en mémoire, matrices et précision numérique
18 mars Interpolation, recherche des racines et systèmes non linéaires
25 mars Intégration numérique
02 avril Équations différentielles ordinaires et partielles
08 avril Examen

Plan de la séance (indicatif)¶

Horaire Sujet
0:00 – 0:05 Récapitulatif & motivation : pourquoi NumPy ?
0:05 – 1:00 Tableaux NumPy (ndarray)
1:00 – 1:50 Opérations vectorisées & diffusion (broadcasting)
1:50 – 2:00 ☕ Pause
2:00 – 2:30 Indexation, découpage et masques booléens
2:30 – 3:00 Lecture de fichiers avec NumPy + graphiques
3:00 – 3:45 Tableaux 2D, maillages et tracés de surface

0 · Récapitulatif & motivation¶

Les boucles Python sont lentes¶

La séance 2 nous a appris à calculer une moyenne avec une boucle for :

def moyenne(valeurs):
    total = 0
    for v in valeurs:
        total += v
    return total / len(valeurs)

Cela fonctionne parfaitement pour de petites listes. Mais que se passe-t-il avec 1 million de valeurs ?

En Python, chaque itération de boucle a un coût fixe élevé (vérification de type, allocation mémoire…).
NumPy délègue les calculs à du code C compilé — il n'y a presque plus de surcoût d'itération.

Démonstration — boucle Python vs NumPy¶

In [ ]:
import time
import numpy as np   # convention universelle : alias np

# Générer des données avec NumPy (np.random.uniform = uniforme aléatoire)
N = int(1E6)   # 1 million de valeurs
donnees_np = np.random.uniform(200, 300, N)   # tableau NumPy
donnees_py = list(donnees_np)                 # liste Python équivalente

# ── Boucle Python ──────────────────────────────────────────────────────────
t0 = time.perf_counter()          # perf_counter() = horloge haute précision
total = 0
for v in donnees_py:
    total += v
moy_boucle = total / len(donnees_py)
dt_boucle = time.perf_counter() - t0

# ── NumPy ──────────────────────────────────────────────────────────────────
t0 = time.perf_counter()
moy_numpy = np.mean(donnees_np)   # mean() = moyenne
dt_numpy = time.perf_counter() - t0

print("Boucle Python :", round(moy_boucle, 4), "  temps :", round(dt_boucle * 1000, 1), "ms")
print("NumPy         :", round(moy_numpy,  4), "  temps :", round(dt_numpy  * 1000, 1), "ms")
print("Facteur d'accélération :", round(dt_boucle / dt_numpy, 0), "x")

1 · Tableaux NumPy — ndarray¶

Qu'est-ce qu'un tableau NumPy ?¶

Un tableau NumPy (ndarray — n-dimensional array, tableau à n dimensions) est comme une liste Python, mais :

Caractéristique Liste Python Tableau NumPy
Type des éléments peut varier (int, str, …) fixe (tous identiques)
Opérations mathématiques élément par élément avec boucle automatiquement vectorisées
Taille variable (append) fixe à la création
Mémoire dispersée bloc contigu (rapide)

En ingénierie, un tableau représente une mesure tensorielle : déformations, contraintes, températures, etc.
Penser à un tableau NumPy comme à un vecteur ou une matrice mathématique en un ou deux dimensions, mais ils peuvent avoir un nombre arbitraire des dimensions.

Créer un tableau — les fonctions essentielles¶

import numpy as np

# À partir d'une liste Python — np.array()
a = np.array([1.0, 2.0, 3.0, 4.0])   # array = tableau

# Tableau de zéros — np.zeros(n)
z = np.zeros(5)  # → [0. 0. 0. 0. 0.]

# Tableau de uns — np.ones(n)
u = np.ones(4)  # → [1. 1. 1. 1.]
# N valeurs régulièrement espacées entre start et stop — np.linspace()
# linspace = linear space (espace linéaire)
T = np.linspace(20, 1200, 7)   # → [ 20. 216.67 … 1200.]

# Valeurs de start à stop avec un pas — np.arange()
# arange = array range (intervalle tabulé)
pas = np.arange(0, 1.0, 0.2)   # → [0.  0.2  0.4  0.6  0.8]

⚠️ linspace vs arange :

  • linspace(a, b, n) — vous choisissez le nombre de points ; la borne b est incluse.
  • arange(a, b, pas) — vous choisissez le pas ; la borne b est exclue.

Attributs d'un tableau — shape, dtype, ndim¶

Chaque tableau possède des attributs (attributes) — des informations accessibles avec le point :

a = np.array([[1.0, 2.0, 3.0],
              [4.0, 5.0, 6.0]])

a.shape   # shape = forme → (2, 3)  : 2 lignes, 3 colonnes
a.dtype   # dtype = type de données → float64
a.ndim    # ndim = nombre de dimensions → 2
Attribut Signification Exemple
shape dimensions du tableau (5,) pour 1D, (3, 4) pour 2D
dtype type de chaque élément float64, int32, bool
ndim nombre de dimensions 1, 2, 3…

⚠️ shape est un tuple, pas un entier. Pour un tableau 1D de 5 éléments, shape = (5,) — notez la virgule.
Pour obtenir le nombre total d'éléments, utilisez a.size ou a.shape[0] (pour la première dimension).

In [ ]:
# ── Démonstration : création de tableaux ──────────────────────────────────
import numpy as np

# Rampe de température pour un traitement thermique
T_rampe = np.linspace(20.0, 1000.0, 6)   # 6 points de 20 à 1000 °C
print("Rampe T :", T_rampe)
print("shape :", T_rampe.shape)
print("dtype :", T_rampe.dtype)

# Tableau de déformations
eps = np.arange(0.0, 0.025, 0.005)   # pas de 0.5 %
print("Déformations :", eps)

# Tableau 2D — une ligne par éprouvette, colonnes : [E_MPa, Rm_MPa, A_%]
eprouvettes = np.array([
    [210000, 510, 22],   # éprouvette 1
    [208000, 495, 24],   # éprouvette 2
    [211000, 525, 19],   # éprouvette 3
])
print("Tableau 2D shape :", eprouvettes.shape)   # → (3, 3)
print("dtype :", eprouvettes.dtype)

Tableaux 1D et 2D — analogie avec les vecteurs et matrices¶

Objet mathématique NumPy Exemple
Vecteur ligne $[x_1, x_2, x_3]$ Tableau 1D np.array([1, 2, 3]) — shape (3,)
Matrice $m \times n$ Tableau 2D shape (m, n)
Cube de données Tableau 3D shape (p, m, n)

Convention rows/columns pour les tableaux 2D :

A = np.zeros((3, 4))   # 3 lignes, 4 colonnes
#                 ↑ noter le tuple (3, 4) — UNE paire de parenthèses extra

A[1, 2]   # élément à la ligne 1, colonne 2 (indices à partir de 0)
A[0, :]   # première ligne entière
A[:, 2]   # troisième colonne entière

⚠️ np.zeros(3, 4) lève une TypeError — il faut écrire np.zeros((3, 4)) avec un tuple en argument.

À vous de faire les exercices 1.1 et 1.2 !¶

2 · Opérations vectorisées¶

Calcul élément par élément — sans boucle¶

Avec NumPy, les opérations arithmétiques s'appliquent simultanément à tous les éléments :

import numpy as np

forces = np.array([12500, 13200, 11800, 14000])  # N
diametres = np.array([10.0, 10.0, 9.5, 11.0])    # mm

# Calcul vectorisé : une seule ligne, pas de boucle
aires = np.pi * (diametres / 2) ** 2  # mm²
contraintes = forces / aires          # MPa

print(contraintes)   # → [159.15  167.93  166.55  147.26]

En Python pur, il faudrait une boucle for ou une compréhension de liste.
NumPy est 10–100× plus rapide et plus lisible.

Fonctions universelles (ufuncs)¶

NumPy fournit des fonctions universelles (universal functions, abrégées ufuncs) qui s'appliquent élément par élément à un tableau :

Fonction Signification Exemple
np.sqrt(a) racine carrée (square root) np.sqrt(np.array([4, 9])) → [2. 3.]
np.exp(a) exponentielle np.exp(0) → 1.0
np.log(a) logarithme naturel np.log(np.e) → 1.0
np.log10(a) logarithme décimal np.log10(1000) → 3.0
np.abs(a) valeur absolue np.abs(-3.5) → 3.5
np.sin(a) sinus (angle en radians) np.sin(np.pi / 2) → 1.0
np.cos(a) cosinus (angle en radians) np.cos(np.pi/2) → 0.0
np.tan(a) tangent (angle en radians) np.tan(np.pi/4) → 1.0
temperatures = np.linspace(300, 1200, 5)   # K
Ea = 150e3  # J/mol — énergie d'activation
R  = 8.314  # J/(mol·K)
A0 = 1e12   # facteur pré-exponentiel

# Arrhenius vectorisé — calcule k pour chaque T en une ligne
k = A0 * np.exp(-Ea / (R * temperatures))

⚠️ Utilisez toujours np.exp, np.log, np.sqrt etc. sur des tableaux NumPy.
math.exp ne fonctionne que sur des scalaires et lève une TypeError sur un tableau.

Agrégations — np.mean, np.std, np.max, np.min¶

Les agrégations (aggregations) condensent un tableau en une seule valeur (ou un vecteur si on précise l'axe) :

durete = np.array([245, 251, 238, 260, 249, 253, 247, 255, 241, 258])

np.mean(durete)  # mean = moyenne → 249.7
np.std(durete)   # std = écart-type (population) → 6.97
np.max(durete)   # → 260
np.min(durete)   # → 238
np.sum(durete)   # → 2497

Argument axis= — pour les tableaux 2D :

# Tableau 2D : lignes = éprouvettes, colonnes = [E, Rm, A]
data = np.array([[210000, 510, 22],
                 [208000, 495, 24],
                 [211000, 525, 19]])

np.mean(data, axis=0)   # moyenne de chaque colonne → [209667, 510, 21.67]
np.mean(data, axis=1)   # moyenne de chaque ligne   → [70177, 69173, 70181]

Mémorisez ainsi : axis=0 réduit la première dimension (les lignes disparaissent) ; axis=1 réduit la deuxième (les colonnes disparaissent).

Diffusion (broadcasting)¶

La diffusion (broadcasting) est la règle qui détermine comment NumPy combine des tableaux de formes différentes.

Cas le plus courant — scalaire + tableau :

T = np.array([20, 200, 400, 600, 800])   # °C
T_kelvin = T + 273.15                    # → [293.15, 473.15, …]
# Le scalaire 273.15 est "diffusé" sur tous les éléments

Deuxième cas — vecteur 1D + tableau 2D :

data   = np.array([[500, 22, 245],
                   [510, 20, 251],
                   [495, 24, 238]])  # shape (3, 3)

moyennes = np.mean(data, axis=0)  # shape (3,) → [501.67, 22.0, 244.67]

# "Centrer" chaque colonne (voir sa position rélatif à la moyenne) en une ligne :
data_centree = data - moyennes  # (3,3) - (3,) → broadcasting automatique

La règle générale : deux dimensions sont compatibles si elles sont égales, ou si l'une d'elles vaut 1.
NumPy étend (diffuse) la dimension de taille 1 pour la rendre égale à l'autre.

In [ ]:
# ── Démonstration : opérations vectorisées & agrégations ──────────────────
import numpy as np

# Données de traction — 5 éprouvettes cylindriques
forces = np.array([12500, 13600, 11900, 14200, 12800])    # N
diametres = np.array([10.0,  10.5,   9.8,  11.2,  10.1])  # mm

# Calcul vectorisé des contraintes
aires = np.pi * (diametres / 2) ** 2  # mm²
contraintes = forces / aires          # MPa
print("Contraintes (MPa) :", np.round(contraintes, 1))

# Agrégations
print("Moyenne  :", round(np.mean(contraintes), 2), "MPa")
print("Écart-type:", round(np.std(contraintes), 2), "MPa")
print("Maximum  :", round(np.max(contraintes), 2),  "MPa")
print("Minimum  :", round(np.min(contraintes), 2),  "MPa")

# Arrhenius — vectorisé sur un tableau de températures
T  = np.linspace(400, 1200, 5)  # K
Ea = 120e3                      # J/mol
R  = 8.314                      # J/(mol·K)
A0 = 1e10                       # 1/s
k  = A0 * np.exp(-Ea / (R * T))
print("\nConstante cinétique k :", np.round(k, 3))

⚠️ Erreurs de forme fréquentes avec broadcasting¶

NumPy vérifie la compatibilité des formes avant tout calcul. Une incompatibilité lève immédiatement une ValueError — c'est utile car elle signale un bug logique plutôt que de produire un résultat silencieusement erroné.

Règle de compatibilité (rappel) :
Deux dimensions sont compatibles si elles sont égales, ou si l'une d'elles vaut 1.
La vérification se fait dimension par dimension en partant de la droite.

Cas 1 — longueurs incompatibles (erreur la plus courante)

import numpy as np

forces = np.array([12500, 13600, 11900, 14200])  # shape (4,)
diametres = np.array([10.0, 10.5, 9.8])          # shape (3,)

# MAUVAIS — (4,) et (3,) sont incompatibles
aires = np.pi * (diametres / 2) ** 2
contraintes = forces / aires
# → ValueError: operands could not be broadcast together with shapes (4,) (3,)

Cas 2 — vecteur 1D soustrait d'un tableau 2D dans le mauvais sens

data = np.array([[500, 22], [510, 20], [495, 24]])  # shape (3, 2)
moyenne = np.array([501.67, 22.0, 24.0])  # shape (3,) ← mauvaise forme

centree = data - moyenne
# → ValueError: operands could not be broadcast together with shapes (3,2) (3,)
# NumPy essaie d'aligner par la droite : (3, 2) vs (3,) → (3, 2) vs (1, 3) → incompatible

Corrections pour le cas 2

# Soustraire la moyenne de chaque colonne — shape (2,) compatible avec (3, 2)
moy_col = np.mean(data, axis=0)  # shape (2,)
centree = data - moy_col  # (3, 2) - (2,) → NumPy aligne par la droite → OK

# Soustraire la moyenne de chaque ligne — il faut shape (3, 1)
moy_lig = np.mean(data, axis=1, keepdims=True)  # keepdims=True conserve la dimension → shape (3, 1)
centree = data - moy_lig  # (3, 2) - (3, 1) → NumPy diffuse la colonne → OK

Réflexe de débogage : avant toute opération entre tableaux, imprimez a.shape et b.shape.
keepdims=True est l'argument qui évite de perdre une dimension lors d'une agrégation.

À vous de faire les exercices 2.1 et 2.2 !¶

☕ Pause — 10 minutes¶

3 · Indexation, découpage et masques booléens¶

Indexation et découpage (slicing) — pareil que les listes, mais plus puissant¶

a = np.array([10, 20, 30, 40, 50, 60, 70, 80])
a[2]     # → 30          (un seul élément)
a[-1]    # → 80          (dernier élément — indice négatif)
a[2:5]   # → [30, 40, 50] (indices 2, 3, 4 — borne haute exclue)
a[::2]   # → [10, 30, 50, 70]  (un sur deux)
a[::-1]  # → [80, 70, … 10]    (ordre inversé)

B = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]])

B[1, 2]     # → 6        (ligne 1, colonne 2)
B[0, :]     # → [1, 2, 3]  (première ligne)
B[:, 1]     # → [2, 5, 8]  (deuxième colonne)
B[0:2, 1:]  # → [[2,3],[5,6]]  (sous-matrice)

⚠️ Le découpage NumPy renvoie une vue (view), pas une copie. Modifier la sous-matrice modifie aussi le tableau original !
Pour une copie indépendante : B[0:2, :].copy(). Ça donne une copie "profonde" où les manipulations sur les copies ne changent pas l'original.

Masques booléens (boolean masks)¶

Un masque booléen est un tableau de True/False obtenu en appliquant une condition à un tableau.
On l'utilise pour filtrer les éléments qui satisfont la condition.

UTS = np.array([480, 530, 510, 470, 560, 490, 615, 505])

# Étape 1 — créer le masque
masque = UTS > 500  # → [False  True  True  False  True  False  True  True]

# Étape 2 — appliquer le masque pour sélectionner
au_dessus = UTS[masque]  # → [530, 510, 560, 615, 505]

# En une ligne
au_dessus = UTS[UTS > 500]

# Compter → np.sum compte les True
nb = np.sum(UTS > 500)  # → 5

Pourquoi cela fonctionne ? Python traite True comme 1 et False comme 0.
Ainsi np.sum(masque) compte exactement le nombre de True.

Sélection conditionnelle — np.where()¶

np.where(condition, valeur_si_vrai, valeur_si_faux) (où) construit un nouveau tableau en choisissant entre deux valeurs selon la condition :

UTS = np.array([480, 530, 510, 470, 560, 490])

# Classer : "conforme" si UTS >= 500, sinon "non conforme"
labels = np.where(UTS >= 500, "conforme", "non conforme")
# → ['non conforme'  'conforme'  'conforme'  'non conforme'  'conforme'  'non conforme']

# Remplacer les valeurs hors-spec par NaN
filtre = np.where(UTS >= 500, UTS, np.nan)
# → [nan 530. 510. nan 560. nan]

Avec un seul argument, np.where(condition) retourne les indices des éléments True :

indices = np.where(UTS >= 500)  # → (array([1, 2, 4]),)
# Utile pour récupérer la position des éléments intéressants
premier_conforme = UTS[np.where(UTS >= 500)[0][0]]  # → 530
In [ ]:
# ── Démonstration : masques booléens & np.where ────────────────────────────
import numpy as np

# Tableau 2D : 8 éprouvettes × 2 propriétés [UTS (MPa), allongement (%)]
specimens = np.array([[480, 14], [530, 11], [510, 16], [470,  9],
                      [560, 18], [490, 13], [615,  8], [505, 15]])

UTS = specimens[:, 0]           # première colonne
allongements = specimens[:, 1]  # deuxième colonne

# Filtrer : UTS > 500 MPa
print("UTS > 500 MPa         :", UTS[UTS > 500])
print("Nombre de specimens   :", np.sum(UTS > 500))
print("UTS moyen (>500 seul.):", round(np.mean(UTS[UTS > 500]), 1), "MPa")

# Filtrer sur les deux colonnes : UTS ≥ 495 ET allongement ≥ 12 %
masque_double = (UTS >= 495) & (allongements >= 12)  # & = ET logique sur tableaux
print("Conformes (UTS≥495 ET A≥12%) :")
print(specimens[masque_double])

# Lignes où allongement > 15 %
print("Lignes A > 15% :", np.where(allongements > 15))

⚠️ Pièges avec les masques booléens¶

1. Utiliser and/or Python au lieu de &/| NumPy

# MAUVAIS — lève une ValueError ambiguë
masque = (UTS >= 500) and (allongements >= 12)

# BON — opérateurs bit-à-bit pour les tableaux NumPy
masque = (UTS >= 500) & (allongements >= 12)  # & = ET
masque = (UTS >= 500) | (allongements >= 12)  # | = OU
masque = ~(UTS >= 500)                        # ~ = NON (négation)

2. Oublier les parenthèses

# MAUVAIS — priorité des opérateurs erronée
masque = UTS >= 500 & allongements >= 12
# Python évalue : UTS >= (500 & allongements) >= 12  ← incorrect !

# BON
masque = (UTS >= 500) & (allongements >= 12)

À vous de faire les exercices 3.1 et 3.2 !¶

4 · Lecture de fichiers avec NumPy¶

np.loadtxt() — lire un CSV simple¶

Pour les fichiers CSV sans en-tête compliqué, np.loadtxt() (charger texte) est le moyen le plus direct :

import numpy as np

data = np.loadtxt("donnees_traction.csv",
                  delimiter=",",  # delimiter = séparateur (virgule)
                  skiprows=3,     # skiprows = ignorer les 3 premières lignes
                  comments="#")   # comments = ignorer les lignes commençant par #
# data est un tableau 2D : une ligne par point, une colonne par variable
deformations = data[:, 0]  # première colonne
contraintes  = data[:, 1]  # deuxième colonne

np.genfromtxt() — fichiers avec données manquantes¶

data = np.genfromtxt("mesures.csv",
                     delimiter=",",
                     skip_header=1,          # skip_header = ignorer la ligne d'en-tête
                     filling_values=np.nan)  # remplacer les manquants par NaN

loadtxt vs genfromtxt :

  • loadtxt — plus rapide, mais échoue si une valeur est manquante
  • genfromtxt — plus souple, gère les valeurs manquantes avec filling_values. Il faut bien spécifier tels valeurs pour ne pas les confondre avec des données réelles (donc utilisez NaN et pas zéro)
import numpy as np
import matplotlib.pyplot as plt

# 1. Charger
data = np.loadtxt("essai_traction_316L.csv", delimiter=",", comments="#")
# Colonnes du fichier : force (N), déplacement (mm)
force       = data[:, 0]
deplacement = data[:, 1]

# Dimensions de l'éprouvette
d0 = 10.0  # mm — diamètre initial
L0 = 50.0  # mm — longueur de jauge initiale

# 2. Calculer — contrainte et déformation d'ingénierie
A0          = np.pi * (d0 / 2) ** 2  # mm²
contrainte  = force / A0             # MPa
deformation = deplacement / L0       # sans unité

# 3. Tracer
plt.figure(figsize=(8, 5))
plt.plot(deformation, contrainte, color='steelblue', linewidth=2, label='316L')
plt.xlabel("Deformation e (sans unite)", fontsize=13)
plt.ylabel("Contrainte s (MPa)",         fontsize=13)
plt.title("Courbe contrainte-deformation — acier 316L", fontsize=14)
plt.legend(fontsize=12)
plt.grid(True)
plt.tight_layout()
plt.show()
In [ ]:
# ── Démonstration : np.loadtxt + calcul + tracé ────────────────────────────
import numpy as np
import matplotlib.pyplot as plt

# Charger le fichier CSV de la séance 2
data = np.loadtxt("donnees_traction.csv", delimiter=",", comments="#", skiprows=1)
# skiprows=1 — ignorer la ligne d'en-tête "deformation,contrainte"

deformations = data[:, 0]
contraintes  = data[:, 1]

print("Points chargés :", len(deformations))
print("Déformation max :", round(np.max(deformations), 4))
print("Contrainte max :", round(np.max(contraintes), 1), "MPa")
print("Contrainte moy :", round(np.mean(contraintes), 1), "MPa")

# Tracé
plt.figure(figsize=(8, 5))
plt.plot(deformations, contraintes, color='steelblue', linewidth=2, marker='o', markersize=4, label='Acier 316L')
plt.xlabel("Deformation ", fontsize=13)
plt.ylabel("Contrainte (MPa)",         fontsize=13)
plt.title("Courbe contrainte-deformation — acier 316L", fontsize=14)
plt.legend(fontsize=12)
plt.grid(True)
plt.tight_layout()
plt.show()

À vous de faire les exercices 4.1 et 4.2 !¶

5 · Tableaux 2D, maillages et tracés de surface¶

Motivation — visualiser une propriété sur deux variables¶

En ingénierie, de nombreuses propriétés dépendent de deux paramètres simultanément :

  • La constante cinétique $k$ dépend de la température $T$ et de l'énergie d'activation $E_a$ (Arrhenius)
  • La résistance dépend de la composition et du temps de traitement thermique
  • La déflexion d'une plaque dépend des coordonnées $x$ et $y$

Pour visualiser cela, on crée une grille de points $(T_i, E_{a,j})$ et on calcule la propriété en chaque point.

NumPy fournit np.meshgrid() (maillage) pour créer cette grille automatiquement.

np.meshgrid() — créer une grille 2D¶

import numpy as np

# Deux vecteurs 1D définissant les axes
x = np.array([1, 2, 3])  # 3 valeurs
y = np.array([10, 20])   # 2 valeurs

# meshgrid produit deux tableaux 2D de forme (ny, nx)
X, Y = np.meshgrid(x, y)
#
# X → [[1, 2, 3], Y → [[10, 10, 10],
#      [1, 2, 3]]      [20, 20, 20]]
#
# En chaque point (X[i,j], Y[i,j]) on a les coordonnées du nœud de la grille.

# Calculer une propriété sur la grille — pas de boucle !
Z = X ** 2 + Y
# Z[i,j] = X[i,j]² + Y[i,j]  pour toute la grille

Pourquoi deux tableaux 2D ?
Parce que pour calculer Z(x, y), on a besoin des valeurs de x et y en chaque nœud. meshgrid "étire" chaque vecteur pour couvrir toute la grille.

Tracés 2D — contours et cartes de chaleur¶

Matplotlib offre plusieurs tracés pour visualiser Z(x, y) :

import matplotlib.pyplot as plt

# 1. Contours colorés — plt.contourf()
# contourf = contour filled (contours remplis)
plt.figure(figsize=(7, 5))
cf = plt.contourf(X, Y, Z, levels=20, cmap='viridis')
plt.colorbar(cf, label='Valeur de Z')  # colorbar = barre de couleurs
plt.xlabel("x", fontsize=12)
plt.ylabel("y", fontsize=12)
plt.title("Carte de Z(x, y)", fontsize=13)
plt.tight_layout()
plt.show()

# 2. Carte de chaleur (*heatmap*) — plt.pcolormesh()
# pcolormesh = pseudocolor mesh (maille pseudocouleur)
plt.figure(figsize=(7, 5))
pc = plt.pcolormesh(X, Y, Z, cmap='plasma', shading='auto')
plt.colorbar(pc)
plt.tight_layout()
plt.show()
Tracé Usage
contourf Mettre en évidence des iso-valeurs (ex. contours de température)
pcolormesh Vue rapide de la distribution sur une grille régulière
plot_surface (3D) Perspective tridimensionnelle pour les présentations "marketing"

Surface 3D — Axes3D et plot_surface()¶

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D  # Axes3D = axes tridimensionnel

fig = plt.figure(figsize=(8, 6))
ax  = fig.add_subplot(111, projection='3d')  # projection = '3d'

ax.plot_surface(X, Y, Z,
                cmap='viridis',    # colormap (palette de couleurs)
                edgecolor='none',  # edgecolor = couleur des arêtes
                alpha=0.9)         # alpha = transparence

ax.set_xlabel("x", fontsize=11)
ax.set_ylabel("y", fontsize=11)
ax.set_zlabel("Z", fontsize=11)  # set_zlabel = étiquette de l'axe z
ax.set_title("Surface Z(x,y)", fontsize=13)
plt.tight_layout()
plt.show()

⚠️ La surface 3D est utile pour les présentations, mais un contourf ou pcolormesh est souvent plus lisible pour lire des valeurs précises.

In [ ]:
# ── Démonstration : Arrhenius sur une grille T × Ea ──
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# ── Données physiques ───
R  = 8.314  # J/(mol·K)
A0 = 1e13   # facteur pré-exponentiel

T_vec  = np.linspace(300, 1200, 80)    # températures : 300–1200 K
Ea_vec = np.linspace(50e3, 250e3, 80)  # E_a : 50–250 kJ/mol

T_grid, Ea_grid = np.meshgrid(T_vec, Ea_vec)  # grilles 2D

# Constante cinétique d'Arrhenius — vectorisé sur toute la grille
k_grid = A0 * np.exp(-Ea_grid / (R * T_grid))

# ── Tracé : contours remplis ─
plt.figure(figsize=(8, 5))
cf = plt.contourf(T_grid, Ea_grid / 1e3, np.log10(k_grid),
                  levels=20, cmap='viridis')
plt.colorbar(cf, label='log10(k)')
plt.xlabel("Temperature T (K)", fontsize=12)
plt.ylabel("Energie d activation Ea (kJ/mol)", fontsize=12)
plt.title("Constante cinematique d Arrhenius log10(k)", fontsize=13)
plt.tight_layout()
plt.show()
In [ ]:
# ── Surface 3D ──
fig = plt.figure(figsize=(8, 6))
ax  = fig.add_subplot(111, projection='3d')
ax.plot_surface(T_grid, Ea_grid / 1e3, np.log10(k_grid),
                cmap='viridis', edgecolor='none', alpha=0.85)
ax.set_xlabel("T (K)", fontsize=10)
ax.set_ylabel("Ea (kJ)", fontsize=10)
ax.set_zlabel("log10(k)", fontsize=10)
ax.set_title("Arrhenius — surface 3D", fontsize=12)
plt.tight_layout()
plt.show()

Choisir une palette de couleurs (colormap)¶

Le choix de la palette influence fortement la lisibilité et l'accessibilité d'un graphique :

Palette Type Usage recommandé
viridis séquentielle valeurs de 0 jusqu'à un max — par défaut
plasma séquentielle similaire à viridis, contraste élevé
coolwarm divergente valeurs positives et négatives (ex. écarts)
RdBu divergente rouge–blanc–bleu, fréquent en météo/matériaux
hot séquentielle températures

Règle d'or : évitez jet (arc-en-ciel) — il crée de fausses perceptions des gradients et est inaccessible aux daltoniens.
viridis et plasma sont perceptivement uniformes et lisibles en noir-et-blanc.

À vous de faire l'exercice 5 !¶

Points clés de la séance¶

Concept Ce qu'il faut retenir
np.array() Convertit une liste en tableau ; tous les éléments ont le même dtype
np.linspace(a,b,n) n points entre a et b inclus
shape, dtype, ndim Attributs fondamentaux — à vérifier avant tout calcul
Opérations vectorisées +, -, *, /, ** opèrent élément par élément — aucune boucle
np.exp, np.sqrt… Utiliser les ufuncs NumPy sur les tableaux
Agrégations + axis= np.mean(A, axis=0) = moyenne de chaque colonne
Masques booléens a[a > seuil] — extraire sans boucle ; combiner avec & et \|
Vue vs copie Le découpage NumPy renvoie une vue ; utilisez .copy() si besoin
np.loadtxt() Lire un CSV propre en une ligne ; skiprows pour l'en-tête
np.meshgrid() Créer une grille 2D à partir de deux vecteurs 1D
contourf / pcolormesh Visualiser Z(x,y) sur une grille — toujours ajouter colorbar