%% nav-debut : généré par gen_navigation.py, ne pas modifier à la main %%
> [!nav]
> ↩ [[Analyse et Données/Mathématiques/Programmation Scientifique/Programmation Scientifique|Accueil · Programmation Scientifique]]
>
> | | [[Analyse et Données/Mathématiques/Programmation Scientifique/Exercices - Programmation scientifique\|Exercices - Programmation scientifique →]] |
> |:---|---:|
%% nav-fin %%
# Cours : Programmation Scientifique
**Objectif :** comprendre les objets mathématiques et les mettre en œuvre avec Python, à partir de définitions, d'exemples calculés et de programmes exécutables.
Les blocs de code peuvent être exécutés dans VS Code ou dans un notebook. Bibliothèques utiles : `numpy`, `scipy`, `sympy`, `matplotlib`.
## 1. Fonctions Python et structures de contrôle
### Valeur renvoyée, boucle et accumulation
**Notions mathématiques.** Une boucle d'accumulation traduit souvent une somme $S_n=\sum_{k=1}^{n}u_k$. Par exemple, $\sum_{k=1}^{4}k^2=1+4+9+16=30$. En Python, `return` fournit la valeur mathématique calculée par la fonction, tandis que `print` l'affiche.
```python
def somme_carres(n):
s = 0
for k in range(1, n + 1):
s += k**2
return s
print(somme_carres(4))
```
**Sortie :** `30`.
`return` termine la fonction et renvoie une valeur ; `print` ne fait qu'afficher. `range(1, n+1)` parcourt les entiers de `1` à `n` inclus. L'opérateur `+=` met à jour un accumulateur.
```python
def calcul(n):
total = 0
for k in range(1, n+1):
if k % 2 == 0:
total += k*k
else:
total -= k
return total
print(calcul(4))
```
**Résultat de l’exécution :**
```text
16
```
### Boucle `while` et compteur
**Notions mathématiques.** Une suite définie par récurrence $u_{n+1}=F(u_n)$ se calcule itérativement. Par exemple, $u_0=1$ et $u_{n+1}=2u_n$ donnent $u_1=2$, $u_2=4$, $u_3=8$. Une boucle `while` répète le calcul tant qu'une condition est vérifiée.
```python
u, n = 1, 0
while u < 8:
u = 2*u
n += 1
print(n, u)
```
**Sortie :** `3 8`.
Une boucle `while` se répète tant que sa condition est vraie. Il faut vérifier que le programme fait évoluer les variables de façon à pouvoir s'arrêter.
```python
x = 17
c = 0
while x >= 5:
x -= 5
c += 1
print(c, x)
```
**Résultat de l’exécution :**
```text
3 2
```
### Compréhensions de listes et expressions génératrices
**Notions mathématiques.** Une compréhension construit une famille $[f(k)]_{k\in I}$, tandis qu'une expression génératrice peut être utilisée pour calculer $\sum_{k\in I}f(k)$. Exemple : les carrés des entiers pairs de $0$ à $5$ sont $0,4,16$ et leur somme vaut $20$.
```python
v = [k*k for k in range(6) if k % 2 == 0]
print(v, sum(v))
```
**Sortie :** `[0, 4, 16] 20`.
- `[expression for x in iterable]` construit une liste.
- `[expression for x in iterable if condition]` construit une liste filtrée.
- `sum(expression for x in iterable if condition)` calcule directement une somme, sans créer explicitement une liste.
- `len(sequence)` donne le nombre d'éléments.
```python
valeurs = [k*k-k for k in range(1, 6)]
selection = [v for v in valeurs if v % 3 == 0]
print(valeurs)
print(selection, sum(selection), len(selection))
```
**Résultat de l’exécution :**
```text
[0, 2, 6, 12, 20]
[0, 6, 12] 18 3
```
### `enumerate`, `zip`, `map` et `lambda`
**Notions mathématiques.** Une fonction est une règle qui associe à chaque élément de son domaine une image. Une fonction anonyme `lambda x: ...` représente cette règle sans nom. `map` applique la fonction à plusieurs valeurs ; `zip` associe les éléments de deux suites terme à terme ; `enumerate` associe un indice à chaque élément. Ainsi, pour $f(x)=x^2-2x$, on obtient $f(1)=-1$, $f(2)=0$, $f(3)=3$ et leur somme est $2$.
```python
f = lambda x: x*x - 2*x
print(list(map(f, [1, 2, 3])))
print(sum(map(f, [1, 2, 3])))
print(list(zip([1, 2], [3, 4])))
```
**Sortie :** `[-1, 0, 3]`, `2`, puis `[(1, 3), (2, 4)]`.
- `enumerate(sequence)` produit des couples `(indice, valeur)`, avec un indice commençant à zéro par défaut.
- `zip(x, y)` parcourt les deux séquences en parallèle et s'arrête à la plus courte.
- `lambda x: expression` définit une fonction anonyme sans l'exécuter immédiatement.
- `map(f, sequence)` applique `f` à chaque élément de la séquence ; `sum(...)` peut additionner les valeurs obtenues.
```python
a, b = 4, 2
f = lambda x: x*x - b*x
valeurs = [a, a+1, b]
print([f(x) for x in valeurs])
print(sum(map(f, valeurs)))
print(sum((-1)**i*v for i, v in enumerate([3, 5, 2])))
print(sum(u-v for u, v in zip([5, 6, 7], [2, 8, 1]) if u > v))
```
**Résultat de l’exécution :**
```text
[8, 15, 0]
23
0
9
```
**Attention :** afficher `f` directement produit une représentation de fonction contenant parfois une adresse mémoire variable. Ce n'est pas le résultat mathématique de la fonction : il faut appeler `f(4)`, par exemple.
### Récursivité
**Notions mathématiques.** Une définition par récurrence exprime $u_n$ en fonction de termes précédents. La factorielle vérifie $0!=1$ et $n!=n(n-1)!$ pour $n\geq1$. Une fonction récursive doit posséder un **cas de base** pour s'arrêter.
```python
def fact(n):
if n == 0:
return 1
return n * fact(n - 1)
print(fact(4))
```
**Sortie :** `24` car $4!=4\times3\times2\times1$.
Une fonction récursive s'appelle elle-même. Elle doit avoir un **cas d'arrêt** ; pour calculer son résultat, on déroule les appels puis on remonte.
```python
def f(n):
if n <= 1:
return 1
return n + f(n-2)
print(f(5))
```
**Résultat de l’exécution :**
```text
9
```
Ici, `f(5) = 5 + f(3) = 5 + 3 + f(1) = 9`.
### Dictionnaires et arguments par défaut
**Notions mathématiques.** Un dictionnaire peut représenter une fonction définie sur un ensemble fini, par exemple $f(1)=4$, $f(2)=9$. Une valeur par défaut dans une fonction Python fixe un paramètre lorsque l'utilisateur ne le fournit pas.
```python
valeurs = {1: 4, 2: 9}
def affine(x, a=2):
return a*x + 1
print(valeurs[2], affine(3), affine(3, 4))
```
**Sortie :** `9 7 13`.
Un dictionnaire associe des clés à des valeurs. `d.items()` permet de parcourir simultanément les clés et les valeurs. Un argument par défaut est utilisé lorsque l'appel ne fournit pas cet argument.
```python
def decale(x, shift=2):
return [v + shift for v in x[::-1]]
d = {'x': 4, 'y': 2, 'z': 6}
print(sum(v for k, v in d.items() if k != 'y'))
print(decale([4, 2, 6]))
print(decale([4, 2, 6], shift=1))
```
**Résultat de l’exécution :**
```text
10
[8, 4, 6]
[7, 3, 5]
```
### Compréhensions imbriquées
**Notions mathématiques.** Deux indices permettent de parcourir un produit cartésien $I\times J$. Par exemple, la matrice $A=(i+j)_{0\leq i,j\leq2}$ vaut $\begin{pmatrix}0&1&2\\1&2&3\\2&3&4\end{pmatrix}$.
```python
A = [[i + j for j in range(3)] for i in range(3)]
print(A)
```
**Sortie :** `[[0, 1, 2], [1, 2, 3], [2, 3, 4]]`.
L'ordre des deux boucles est important : la boucle extérieure produit les lignes, la boucle intérieure les éléments de chaque ligne.
```python
a, b = 2, 3
M = [[a+i+j*b for j in range(3)] for i in range(3)]
print(M)
print(sum(M[i][i] for i in range(3)))
```
**Résultat de l’exécution :**
```text
[[2, 5, 8], [3, 6, 9], [4, 7, 10]]
18
```
### Définir un modèle paramétré
Considérons la famille de fonctions
$f_{a,b}(x)=e^{-ax}\cos(bx).$
```python
import math
def f(x, a, b):
return math.exp(-a*x) * math.cos(b*x)
for x in [0, 0.5, 1, 2]:
print(f'x={x:4.1f}, f(x)={f(x, 0.3, 2): .6f}')
```
**Résultat de l’exécution :**
```text
x= 0.0, f(x)= 1.000000
x= 0.5, f(x)= 0.465043
x= 1.0, f(x)=-0.308289
x= 2.0, f(x)=-0.358727
```
## 2. Calcul numérique et fonctions mathématiques
### La bibliothèque `math`
**Notions mathématiques.** Pour un réel $x$, $\lfloor x\rfloor$ désigne le plus grand entier inférieur ou égal à $x$, $\sqrt{x}$ est défini pour $x\geq0$, $e^x$ est l'exponentielle et $\ln(x)$ le logarithme népérien défini pour $x>0$. Les fonctions trigonométriques utilisent les radians : $\sin(\pi/2)=1$. La factorielle d'un entier $n\geq0$ est $n!=1\times2\times\cdots\times n$, avec $0!=1$. Par exemple, $5!=120$.
```python
import math
print(math.sin(math.pi / 2))
print(math.factorial(5))
```
**Sortie :** `1.0` puis `120`.
```python
import math
x = math.pi / 3
print('sin(x) =', math.sin(x))
print('exp(-x) =', math.exp(-x))
print('ln(2) =', math.log(2))
print('sqrt(5) =', math.sqrt(5))
print('5! =', math.factorial(5))
```
**Résultat de l’exécution :**
```text
sin(x) = 0.8660254037844386
exp(-x) = 0.350919807178411
ln(2) = 0.6931471805599453
sqrt(5) = 2.23606797749979
5! = 120
```
Les fonctions trigonométriques utilisent des **radians**. Attention aux nombres flottants : deux résultats mathématiquement égaux peuvent différer légèrement en machine.
```python
print(0.1 + 0.2 == 0.3)
print(math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-12))
```
**Résultat de l’exécution :**
```text
False
True
```
### Quotient, reste et arrondis
**Définition mathématique — division euclidienne.** Pour deux entiers $a$ et $b>0$, il existe un unique couple d'entiers $(q,r)$ tel que $a=bq+r$ et $0\leq r<b$. $q$ est le quotient et $r$ le reste. Par exemple, $17=5\times3+2$ : quotient $3$, reste $2$. La fonction plancher $\lfloor x\rfloor$ est le plus grand entier $\leq x$ ; ainsi $\lfloor-3,5\rfloor=-4$.
```python
import math
print(divmod(17, 5))
print(17 // 5, 17 % 5, math.floor(-3.5))
```
**Sortie :** `(3, 2)` puis `3 2 -4`.
- `a // b` donne le quotient de la division entière par arrondi vers le bas (vers \(-\infty\)).
- `a % b` donne le reste associé : `a == (a // b)*b + a % b` pour `b != 0`.
- `divmod(a, b)` renvoie le couple `(a // b, a % b)`.
- `math.floor(x)` renvoie le plus grand entier inférieur ou égal à `x` ; `math.ceil(x)` le plus petit entier supérieur ou égal à `x`.
- `math.isqrt(n)` renvoie la racine carrée entière de `n` (pour `n >= 0`).
```python
import math
x = 17
q, r = divmod(x, 5)
print(q, r)
print(math.floor(-7/2), math.ceil(-7/2))
print(math.isqrt(17))
```
**Résultat de l’exécution :**
```text
3 2
-4 -3
4
```
**Point de vigilance :** `-7 // 2` vaut `-4` et non `-3`. Ne pas confondre `//` et la troncature vers zéro.
## 3. Arithmétique, combinatoire et calcul exact
### PGCD, PPCM, factorielles et combinaisons
**Définition mathématique — PGCD.** Un entier $d>0$ est un diviseur commun de $a$ et $b$ si $d\mid a$ et $d\mid b$. Le **plus grand commun diviseur** $\operatorname{pgcd}(a,b)$ est le plus grand de ces diviseurs (pour $a,b$ non tous deux nuls). Par exemple, les diviseurs positifs communs à $18$ et $24$ sont $1,2,3,6$ : $\operatorname{pgcd}(18,24)=6$. Le **PPCM** est le plus petit entier strictement positif multiple des deux nombres : $\operatorname{ppcm}(18,24)=72$. Pour $a,b>0$, $\operatorname{pgcd}(a,b)\operatorname{ppcm}(a,b)=ab$.
```python
import math
print(math.gcd(18, 24))
print(math.lcm(18, 24))
```
**Sortie :** `6` puis `72`.
**Définition mathématique — factorielle et combinaisons.** Pour $n\geq1$, $n!=\prod_{k=1}^{n}k$ et $0!=1$. Le coefficient binomial $\binom nk$ compte les sous-ensembles de $k$ éléments d'un ensemble de $n$ éléments. Ainsi $\binom 52=5!/(2!3!)=10$.
```python
print(math.factorial(5), math.comb(5, 2))
```
**Sortie :** `120 10`.
- `math.gcd(a, b)` calcule le PGCD ; `math.lcm(a, b)` calcule le PPCM.
- `math.factorial(n)` calcule `n!` pour un entier `n >= 0`.
- `math.comb(n, k)` calcule le coefficient binomial \(\binom nk\).
- `x**n` signifie puissance ; `x*x` signifie multiplication.
```python
import math
n, k = 6, 2
c = math.comb(n, k)
r = math.factorial(k + 1) // math.factorial(k - 1)
print(c, r)
print(math.gcd(18, 24), math.lcm(18, 24))
print(r - k if r % 2 == 0 else r + k)
```
**Résultat de l’exécution :**
```text
15 6
6 72
4
```
La dernière instruction utilise une **expression conditionnelle** : `valeur_si_vrai if condition else valeur_si_faux`. Ici, la condition est évaluée avant de choisir l'une des deux expressions.
**Notions mathématiques.** Une permutation de $n$ objets distincts se compte par $n!$. Le nombre de choix de $k$ objets parmi $n$, sans ordre et sans répétition, est le coefficient binomial $\binom nk=\frac{n!}{k!(n-k)!}$. Par exemple, $\binom 52=10$. Une fraction rationnelle $p/q$ ($q\neq0$) peut être représentée exactement sans erreur d'arrondi.
```python
import math
from fractions import Fraction
print(math.comb(5, 2))
print(Fraction(1, 3) + Fraction(1, 6))
```
**Sortie :** `10` puis `1/2`.
```python
import math
from itertools import permutations, combinations, product
print('C(8,3) =', math.comb(8, 3))
print('A(8,3) =', math.perm(8, 3))
print('3! =', math.factorial(3))
print(list(combinations('ABCD', 2)))
print(list(permutations('ABC', 2)))
print(list(product([0, 1], repeat=3)))
```
**Résultat de l’exécution :**
```text
C(8,3) = 56
A(8,3) = 336
3! = 6
[('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]
[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
[(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1)]
```
Les combinaisons ignorent l'ordre, contrairement aux arrangements. Éviter de convertir en liste toutes les possibilités lorsque leur nombre est très grand.
### Fractions exactes et attributs
**Notions mathématiques.** Une fraction $p/q$ représente un nombre rationnel ($p\in\mathbb Z$, $q\in\mathbb Z\setminus\{0\}$). On simplifie une fraction en divisant le numérateur et le dénominateur par leur PGCD : $6/8=3/4$. Pour additionner, on met au même dénominateur : $3/4+2/3=9/12+8/12=17/12$.
```python
from fractions import Fraction
z = Fraction(6, 8) + Fraction(2, 3)
print(z, z.numerator, z.denominator)
```
**Sortie :** `17/12 17 12`.
`Fraction(p, q)` crée une fraction rationnelle exacte. Les attributs `.numerator` et `.denominator` renvoient respectivement le numérateur et le dénominateur **après simplification**.
```python
from fractions import Fraction
x = Fraction(6, 8)
y = Fraction(2, 3)
z = x + y
print(x, z)
print(z.numerator, z.denominator)
```
**Résultat de l’exécution :**
```text
3/4 17/12
17 12
```
`Fraction` conserve les nombres rationnels exacts ; `Decimal` permet de choisir une précision décimale. Ces outils sont utiles pour distinguer une égalité exacte d'une approximation flottante.
```python
from fractions import Fraction
from decimal import Decimal, getcontext
print(Fraction(1, 3) + Fraction(1, 6))
getcontext().prec = 35
print(Decimal(2).sqrt())
print(0.1 + 0.2 == 0.3)
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3'))
```
**Résultat de l’exécution :**
```text
1/2
1.4142135623730950488016887242096981
False
True
```
## 4. Nombres complexes et géométrie
### Nombres complexes avec `cmath`
**Notions mathématiques.** Un nombre complexe s'écrit $z=a+ib$, où $i^2=-1$. Son conjugué est $\overline z=a-ib$, son module $|z|=\sqrt{a^2+b^2}$ et $z\overline z=|z|^2$. Par exemple, pour $z=3+4i$, $|z|=5$ et $z\overline z=25$.
```python
import cmath
z = 3 + 4j
print(abs(z), z * z.conjugate())
```
**Sortie :** `5.0 (25+0j)`.
Python utilise `j` pour l'unité imaginaire. Pour $z=a+ib$, le module est $|z|=\sqrt{a^2+b^2}$, l'argument est $\arg(z)$ et la formule d'Euler donne $e^{i\theta}=\cos\theta+i\sin\theta$.
```python
import cmath
import numpy as np
z = 3 + 4j
print(z.real, z.imag, abs(z), cmath.phase(z))
print(cmath.polar(z), cmath.rect(5, cmath.phase(z)))
theta = np.linspace(0, 2*np.pi, 100)
cercle = np.exp(1j*theta)
print(np.allclose(np.abs(cercle), 1))
```
**Résultat de l’exécution :**
```text
3.0 4.0 5.0 0.9272952180016122
(5.0, 0.9272952180016122) (3.0000000000000004+3.9999999999999996j)
True
```
### Rotations complexes
Multiplier $z$ par $e^{i\theta}$ revient à effectuer une rotation d'angle $\theta$ autour de l'origine.
```python
import matplotlib.pyplot as plt
points = np.array([0+0j, 2+0j, 2+1j, 0+1j, 0+0j])
rotation = points * np.exp(1j*np.pi/4)
plt.plot(points.real, points.imag, 'o-', label='Initial')
plt.plot(rotation.real, rotation.imag, 'o-', label='Rotation')
plt.axis('equal'); plt.grid(); plt.legend(); plt.show()
```
**Résultat de l’exécution :**

### Nombres complexes et conversion
**Notions mathématiques.** Pour $z=a+ib$, $\Re(z)=a$, $\Im(z)=b$, $\overline z=a-ib$ et $z\overline z=a^2+b^2$. La multiplication suit la règle $i^2=-1$ : $(1+2i)(3-i)=5+5i$.
```python
z = (1 + 2j) * (3 - 1j)
print(z, z.real, z.imag)
```
**Sortie :** `(5+5j) 5.0 5.0`.
`complex(a, b)` crée \(a+bi\), `z.conjugate()` renvoie son conjugué, et `z.real` et `z.imag` ses parties réelle et imaginaire. En Python, l'unité imaginaire s'écrit `1j`.
```python
z = complex(3, 2)
w = z * z.conjugate() + (z + 1j)**2
print(w)
print(w.real, w.imag)
print(int(w.real - w.imag))
```
**Résultat de l’exécution :**
```text
(13+18j)
13.0 18.0
-5
```
`int(x)` tronque un réel vers zéro : ce n'est pas un arrondi à l'entier le plus proche. Dans l'examen, les conversions portent sur des résultats choisis pour éviter les ambiguïtés d'arrondi.
## 5. Tableaux NumPy et opérations numériques
### Les tableaux numériques
**Notions mathématiques.** Un vecteur $(x_1,\ldots,x_n)$ est une suite ordonnée de nombres. Un tableau à deux dimensions peut représenter une matrice $A=(a_{ij})$ à $m$ lignes et $n$ colonnes. En Python, les indices commencent à zéro : `A[1, 2]` correspond à l'élément mathématique $a_{2,3}$. La somme d'un tableau est $\sum_i x_i$ ; la moyenne est $\frac1n\sum_i x_i$.
```python
import numpy as np
A = np.array([[1, 2, 3], [4, 5, 6]])
print(A.shape, A[1, 2], A.sum())
```
**Sortie :** `(2, 3) 6 21`.
```python
import numpy as np
v = np.array([1., 2., 3.])
w = np.array([4., 5., 6.])
print('Somme :', v + w)
print('Produit terme à terme :', v * w)
print('Produit scalaire :', np.dot(v, w))
print('Norme :', np.linalg.norm(v))
print('Dimensions :', v.shape)
```
**Résultat de l’exécution :**
```text
Somme : [5. 7. 9.]
Produit terme à terme : [ 4. 10. 18.]
Produit scalaire : 32.0
Norme : 3.7416573867739413
Dimensions : (3,)
```
`np.linspace(a, b, n)` crée `n` valeurs régulièrement espacées entre `a` et `b`. Ici, il sert à **évaluer des fonctions**, pas à construire un schéma de discrétisation.
```python
x = np.linspace(-2, 2, 9)
y = x**3 - 2*x + 1
print(np.column_stack((x, y)))
print('Minimum observé :', np.min(y))
```
**Résultat de l’exécution :**
```text
[[-2. -3. ]
[-1.5 0.625]
[-1. 2. ]
[-0.5 1.875]
[ 0. 1. ]
[ 0.5 0.125]
[ 1. 0. ]
[ 1.5 1.375]
[ 2. 5. ]]
Minimum observé : -3.0
```
### Opérations sur des polynômes
**Notions mathématiques.** Un polynôme est une expression $P(x)=a_nx^n+\cdots+a_1x+a_0$. Son degré est le plus grand exposant dont le coefficient est non nul. L'évaluation consiste à remplacer $x$ par une valeur donnée : si $P(x)=2x^2-3x+1$, alors $P(2)=8-6+1=3$.
```python
import numpy as np
p = np.poly1d([2, -3, 1])
print(p(2))
```
**Sortie :** `3`.
```python
from numpy.polynomial import Polynomial
P = Polynomial([2, -3, 1]) # 2 - 3x + x²
print('P(4) =', P(4))
print('Racines :', P.roots())
print('P² :', P * P)
```
**Résultat de l’exécution :**
```text
P(4) = 6.0
Racines : [1. 2.]
P² : 4.0 - 12.0·x + 13.0·x² - 6.0·x³ + 1.0·x⁴
```
### Construire et réorganiser des tableaux
**Notions mathématiques.** Réorganiser un vecteur de $mn$ composantes en une matrice $m\times n$ ne change pas ses valeurs, seulement leur disposition. Par exemple, les six entiers de $1$ à $6$ peuvent former deux lignes et trois colonnes.
```python
import numpy as np
A = np.arange(1, 7).reshape(2, 3)
print(A)
```
**Sortie :** `[[1 2 3] [4 5 6]]` (sur deux lignes).
- `np.arange(debut, fin)` construit une suite d'entiers en excluant `fin`.
- `.reshape(lignes, colonnes)` change la forme si le nombre d'éléments est compatible.
- `.T` transpose un tableau bidimensionnel.
- `np.eye(n, dtype=int)` crée une matrice identité.
- `np.concatenate((x, y))` concatène deux tableaux.
```python
import numpy as np
A = np.arange(1, 10).reshape(3, 3)
print(A)
print(A.T)
print(np.eye(2, dtype=int))
```
**Résultat de l’exécution :**
```text
[[1 2 3]
[4 5 6]
[7 8 9]]
[[1 4 7]
[2 5 8]
[3 6 9]]
[[1 0]
[0 1]]
```
### Slicing et indices négatifs
**Notions mathématiques.** Le slicing sélectionne une sous-suite $x_i,x_{i+p},\ldots$ selon un pas $p$ ; la borne de fin est exclue. Avec un pas $-1$, on parcourt les indices dans l'ordre inverse. Par exemple, les indices `1:5:2` sélectionnent les positions $1$ et $3$.
```python
import numpy as np
v = np.array([10, 20, 30, 40, 50])
print(v[1:5:2], v[::-1])
```
**Sortie :** `[20 40] [50 40 30 20 10]`.
La syntaxe `x[debut:fin:pas]` sélectionne les éléments depuis `debut` jusqu'à **avant** `fin`. Un pas négatif parcourt les éléments en sens inverse. Pour les matrices, `A[lignes, colonnes]` combine deux sélections.
```python
import numpy as np
A = np.arange(1, 10).reshape(3, 3)
B = A[:, ::-1]
C = B[1:, :2] * 2
print(B)
print(C)
print(int(np.sum(C) - np.min(B)))
```
**Résultat de l’exécution :**
```text
[[3 2 1]
[6 5 4]
[9 8 7]]
[[12 10]
[18 16]]
55
```
### Masques booléens
**Notions mathématiques.** Un masque représente une condition logique $P(x_i)$ et sélectionne les éléments qui la vérifient. Pour $v=(1,2,3,4,5)$, la condition $x_i>2$ sélectionne $(3,4,5)$, dont la somme vaut $12$.
```python
import numpy as np
v = np.arange(1, 6)
print(v[v > 2], v[v > 2].sum())
```
**Sortie :** `[3 4 5] 12`.
`x > seuil` crée un tableau de booléens. L'écriture `x[x > seuil]` extrait les éléments satisfaisant la condition. Le masque peut aussi servir à modifier des éléments.
```python
import numpy as np
x = np.array([2, 5, 3, 7])
print(x > 3)
print(x[x > 3])
x[x > 3] -= 1
print(x)
```
**Résultat de l’exécution :**
```text
[False True False True]
[5 7]
[2 4 3 6]
```
### Broadcasting
**Notions mathématiques.** Le broadcasting étend une opération terme à terme à des tableaux de dimensions compatibles. Ajouter un vecteur $v=(10,20,30)$ à chaque ligne de $A=\begin{pmatrix}1&2&3\\4&5&6\end{pmatrix}$ donne $\begin{pmatrix}11&22&33\\14&25&36\end{pmatrix}$.
```python
import numpy as np
A = np.array([[1, 2, 3], [4, 5, 6]])
print(A + np.array([10, 20, 30]))
```
**Sortie :** `[[11 22 33] [14 25 36]]` (sur deux lignes).
NumPy peut combiner des tableaux de formes différentes lorsque leurs dimensions sont compatibles. Une opération comme `A + v` ajoute un vecteur à chaque ligne si les dimensions conviennent. `u[:, None]` transforme un vecteur de forme `(n,)` en colonne de forme `(n,1)`.
```python
import numpy as np
A = np.array([[2, 1], [4, 3]])
v = np.array([1, 10])
print(A + v)
u = np.array([2, 4])
print(u[:, None] + np.array([1, 2, 3]))
```
**Résultat de l’exécution :**
```text
[[ 3 11]
[ 5 13]]
[[3 4 5]
[5 6 7]]
```
### Réductions suivant un axe
**Notions mathématiques.** Pour une matrice $A=(a_{ij})$, les sommes par colonnes sont $c_j=\sum_i a_{ij}$ et les sommes par lignes $\ell_i=\sum_j a_{ij}$. Dans NumPy, `axis=0` réduit les lignes et conserve une valeur par colonne ; `axis=1` réduit les colonnes et conserve une valeur par ligne.
```python
import numpy as np
A = np.array([[1, 2, 3], [4, 5, 6]])
print(A.sum(axis=0), A.sum(axis=1))
```
**Sortie :** `[5 7 9] [6 15]`.
- `A.sum(axis=0)` additionne les éléments **colonne par colonne**.
- `A.sum(axis=1)` additionne les éléments **ligne par ligne**.
- `np.min(A)` renvoie le minimum ; `np.mean(A)` la moyenne.
- `np.trace(A)` additionne les éléments diagonaux.
- `np.sort(x)` trie ; `np.diff(x)` calcule les différences entre éléments successifs.
```python
import numpy as np
A = np.array([[2, 1, 4], [5, 3, 6]])
print(A.sum(axis=0))
print(A.sum(axis=1))
x = np.array([5, 1, 4, 2])
y = np.sort(x)
print(y, np.diff(y))
```
**Résultat de l’exécution :**
```text
[ 7 4 10]
[ 7 14]
[1 2 4 5] [1 2 1]
```
### Vues, copies et modification en place
**Notions mathématiques et informatiques.** Deux écritures peuvent désigner les mêmes données en mémoire (une **vue**) ou deux ensembles indépendants de valeurs (une **copie**). Une modification de la vue peut modifier le tableau initial. Cette distinction ne change pas l'opération mathématique, mais elle change l'état des variables lors de l'exécution.
```python
import numpy as np
A = np.array([1, 2, 3])
B = A[1:]
C = A.copy()
B[0] = 9
print(A, C)
```
**Sortie :** `[1 9 3] [1 2 3]`.
C'est une distinction essentielle pour comprendre les résultats d'un programme.
- `B = A[::2]` crée généralement une **vue** : `B` et `A` partagent les données sélectionnées.
- `B = A.copy()` crée une **copie indépendante**.
- `B = A[masque]` (indexation booléenne) crée généralement une copie.
- `B += 2` modifie les valeurs de `B` en place ; si `B` est une vue, `A` change aussi.
- Pour une liste de listes, `y = x[:]` copie seulement la liste extérieure : les listes intérieures restent partagées.
```python
import numpy as np
A = np.array([2, 4, 3, 5])
B = A[::2]
C = A.copy()
B += 10
print(A)
print(B)
print(C)
x = [[2, 4], [4, 2]]
y = x[:]
y[0][0] += 2
print(x, y)
```
**Résultat de l’exécution :**
```text
[12 4 13 5]
[12 13]
[2 4 3 5]
[[4, 4], [4, 2]] [[4, 4], [4, 2]]
```
**Contraste important :** `A * B` est un produit élément par élément, tandis que `A @ B` est un produit matriciel.
## 6. Algèbre linéaire numérique
### Calcul matriciel
**Notions mathématiques.** Pour $A\in\mathbb R^{m\times n}$ et $B\in\mathbb R^{n\times p}$, le produit matriciel $C=AB$ vérifie $c_{ij}=\sum_{k=1}^n a_{ik}b_{kj}$. Il ne faut pas le confondre avec le produit terme à terme. La trace d'une matrice carrée est $\operatorname{tr}(A)=\sum_i a_{ii}$. Pour $A=\begin{pmatrix}1&2\\3&4\end{pmatrix}$, la trace vaut $5$ et le déterminant $1\cdot4-2\cdot3=-2$.
```python
import numpy as np
A = np.array([[1, 2], [3, 4]])
print(A @ A)
print(np.trace(A), round(np.linalg.det(A)))
```
**Sortie :** `[[ 7 10] [15 22]]` (sur deux lignes), puis `5 -2`.
```python
import numpy as np
A = np.array([[2., 1.], [1., 3.]])
B = np.array([[0., 1.], [2., 0.]])
print('A + B :\n', A + B)
print('Produit matriciel AB :\n', A @ B)
print('Produit terme à terme :\n', A * B)
print('Transposée :\n', A.T)
print('Déterminant :', np.linalg.det(A))
print('Rang :', np.linalg.matrix_rank(A))
print('Valeurs propres :', np.linalg.eigvals(A))
```
**Résultat de l’exécution :**
```text
A + B :
[[2. 2.]
[3. 3.]]
Produit matriciel AB :
[[2. 2.]
[6. 1.]]
Produit terme à terme :
[[0. 1.]
[2. 0.]]
Transposée :
[[2. 1.]
[1. 3.]]
Déterminant : 5.000000000000001
Rang : 2
Valeurs propres : [1.38196601 3.61803399]
```
### Résolution de systèmes linéaires
**Notions mathématiques.** Résoudre $Ax=b$ consiste à déterminer un vecteur $x$ satisfaisant toutes les équations. Lorsque $A$ est carrée et inversible ($\det A\neq0$), la solution est unique. Par exemple, $x+y=5$ et $x-y=1$ donnent $x=3$, $y=2$.
```python
import numpy as np
A = np.array([[1., 1.], [1., -1.]])
b = np.array([5., 1.])
print(np.linalg.solve(A, b))
```
**Sortie :** `[3. 2.]`.
Pour résoudre $Ax=b$, utiliser `np.linalg.solve(A, b)` plutôt que calculer explicitement $A^{-1}b$.
```python
A = np.array([[3., 1., -1.],
[2., 4., 1.],
[-1., 2., 5.]])
b = np.array([4., 1., 1.])
x = np.linalg.solve(A, b)
print('Solution :', x)
print('Résidu Ax-b :', A @ x - b)
print('Norme du résidu :', np.linalg.norm(A @ x - b))
```
**Résultat de l’exécution :**
```text
Solution : [ 2. -1. 1.]
Résidu Ax-b : [ 0.00000000e+00 4.44089210e-16 -5.55111512e-16]
Norme du résidu : 7.108895957933346e-16
```
**Notions mathématiques.** La puissance matricielle $A^2=A\cdot A$ est un produit matriciel, différent du carré de chaque coefficient. La trace $\operatorname{tr}(A)$ est la somme des coefficients diagonaux ; le déterminant indique notamment si une matrice carrée est inversible. Pour $A=\begin{pmatrix}1&1\\0&2\end{pmatrix}$, $A^2=\begin{pmatrix}1&3\\0&4\end{pmatrix}$ et $\operatorname{tr}(A^2)=5$.
```python
import numpy as np
A = np.array([[1, 1], [0, 2]])
print(np.linalg.matrix_power(A, 2))
print(np.trace(np.linalg.matrix_power(A, 2)))
```
**Sortie :** `[[1 3] [0 4]]` (sur deux lignes), puis `5`.
- `np.dot(u, v)` calcule le produit scalaire de deux vecteurs.
- `A @ B` calcule le produit matriciel ; `A @ v` le produit matrice-vecteur.
- `np.linalg.solve(A, b)` résout le système `A @ x = b` (si la matrice est inversible).
- `np.linalg.det(A)` calcule le déterminant ; `np.trace(A)` la trace.
- `np.linalg.matrix_power(A, n)` calcule une puissance matricielle, contrairement à `A**n` qui agit élément par élément.
- `.diagonal()` extrait la diagonale d'un tableau matriciel.
```python
import numpy as np
A = np.array([[2, 1], [0, 3]])
B = A @ A
print(B)
print(np.trace(B))
print(np.linalg.matrix_power(A, 2))
print(np.dot(np.array([2, 3]), np.array([4, 1])))
M = np.array([[1, 1], [0, 2]])
b = np.array([5, 6])
print(np.linalg.solve(M, b))
```
**Résultat de l’exécution :**
```text
[[4 5]
[0 9]]
13
[[4 5]
[0 9]]
11
[2. 3.]
```
**Précision numérique :** les fonctions `np.linalg` effectuent généralement des calculs flottants. Un résultat mathématiquement entier peut s'afficher `5.999999999999999` ; dans un programme, `round` et `int` n'ont pas le même rôle. `round` arrondit, tandis que `int` tronque vers zéro.
## 7. Calcul symbolique avec SymPy
### Calcul symbolique avec SymPy (initiation)
SymPy conserve des expressions exactes et permet de comparer calcul symbolique et calcul numérique.
```python
import sympy as sp
x = sp.symbols('x')
f = (x - 1)**2 * (x + 2)
print('Développement :', sp.expand(f))
print('Factorisation :', sp.factor(sp.expand(f)))
print('Dérivée symbolique :', sp.diff(f, x))
print('Racines exactes :', sp.solve(f, x))
```
**Résultat de l’exécution :**
```text
Développement : x**3 - 3*x + 2
Factorisation : (x - 1)**2*(x + 2)
Dérivée symbolique : (x - 1)**2 + (x + 2)*(2*x - 2)
Racines exactes : [-2, 1]
```
### Expressions exactes avec SymPy
**Notions mathématiques.** Le développement transforme un produit en somme : $(x+1)^2=x^2+2x+1$. La factorisation réalise l'opération inverse. La dérivée de $x^n$ est $nx^{n-1}$ ; une primitive de $x^n$ ($n\neq-1$) est $x^{n+1}/(n+1)+C$.
```python
import sympy as sp
x = sp.symbols('x')
f = (x + 1)**2
print(sp.expand(f))
print(sp.diff(f, x))
print(sp.integrate(x**2, x))
```
**Sortie :** `x**2 + 2*x + 1`, `2*x + 2`, `x**3/3`.
```python
import sympy as sp
x = sp.symbols('x')
expr = (x+1)**3
print(sp.expand(expr))
print(sp.factor(x**2-5*x+6))
print(sp.solve(sp.Eq(x**2-5*x+6, 0), x))
print(sp.diff(sp.exp(-x)*sp.sin(x), x)) # Dérivation symbolique
print(sp.integrate(x**2, x)) # Primitive symbolique
```
**Résultat de l’exécution :**
```text
x**3 + 3*x**2 + 3*x + 1
(x - 3)*(x - 2)
[2, 3]
-exp(-x)*sin(x) + exp(-x)*cos(x)
x**3/3
```
`sp.diff` et `sp.integrate` effectuent des opérations **symboliques**, pas des approximations par différences finies ou quadrature numérique.
```python
import sympy as sp
x = sp.symbols('x')
P = x**2 - 3*x + 2
print(sp.factor(P))
print(sp.solve(P, x))
print(sp.diff(P, x))
print(sp.integrate(P, x))
```
**Sortie :** `(x - 2)*(x - 1)`, `[1, 2]`, `2*x - 3`, `x**3/3 - 3*x**2/2 + 2*x`.
SymPy manipule des objets symboliques exacts, contrairement aux tableaux NumPy principalement numériques.
- `sp.symbols('x')` crée un symbole mathématique.
- `sp.expand(expr)` développe une expression.
- `sp.factor(expr)` factorise ; `sp.simplify(expr)` tente une simplification.
- `sp.cancel(expr)` simplifie une fraction rationnelle en annulant les facteurs communs.
- `sp.diff(expr, x)` dérive ; `sp.integrate(expr, (x, a, b))` calcule une intégrale définie.
- `expr.subs(x, valeur)` substitue une valeur ; `expr.coeff(x, n)` extrait le coefficient de `x**n`.
- `sp.solve(expr, x)` recherche les solutions de `expr = 0` ; `sp.limit(expr, x, a)` calcule une limite.
- `sp.degree(expr)` donne le degré d'un polynôme.
- `sp.Matrix(...)`, `.det()`, `sp.trace(...)` et `sp.eye(n)` manipulent des matrices exactes.
```python
import sympy as sp
x = sp.symbols('x')
f = (x + 2)**3 - 3*x**2
g = sp.diff(f, x)
print(sp.expand(f))
print(g.subs(x, 1))
print(sp.integrate(2*x + 3, (x, 0, 2)))
print(sp.cancel((x**2 - 4)/(x - 2)))
print(sp.solve((x-2)*(x-5), x))
```
**Résultat de l’exécution :**
```text
x**3 + 3*x**2 + 12*x + 8
21
10
x + 2
[2, 5]
```
### Attention aux coefficients et aux solutions distinctes
```python
import sympy as sp
x = sp.symbols('x')
p = sp.expand((x + 2)**2 * (x + 3))
print(p)
print(p.coeff(x, 2), p.coeff(x, 0))
print(sp.degree(p))
print(sp.solve((x-2)**2, x))
```
**Résultat de l’exécution :**
```text
x**3 + 7*x**2 + 16*x + 12
7 12
3
[2]
```
`sp.solve` renvoie ici la liste des **racines distinctes** : une racine double n'apparaît pas nécessairement deux fois. La longueur `len(sp.solve(...))` n'est donc pas toujours le degré du polynôme.
### Matrices symboliques
```python
import sympy as sp
A = sp.Matrix([[2, 1], [0, 3]])
B = A*A + sp.eye(2)
print(B)
print(sp.trace(B), B.det())
```
**Résultat de l’exécution :**
```text
Matrix([[5, 5], [0, 10]])
15 50
```
## 8. Résolution et optimisation avec SciPy
### Bibliothèque `scipy.optimize`
Pour une fonction $f$, rechercher une racine signifie obtenir un $x$ tel que $f(x)\approx 0$. Minimiser une fonction signifie trouver une valeur $x_*$ pour laquelle $f(x_*)$ est aussi petite que possible sur le domaine choisi. On utilise ici les fonctions existantes, sans étudier les algorithmes internes.
### Rechercher un zéro et un minimum
**Notions mathématiques.** Une racine de $f$ est un réel $x_0$ tel que $f(x_0)=0$. Un minimum local $x_*$ vérifie $f(x_*)\leq f(x)$ pour $x$ suffisamment proche de $x_*$. Pour une fonction dérivable, $f'(x_*)=0$ est une condition nécessaire à un extremum intérieur, mais pas suffisante. Par exemple, $f(x)=(x-2)^2$ admet une racine et un minimum global en $x=2$.
```python
from scipy.optimize import brentq, minimize_scalar
f = lambda x: (x - 2)**2
print(brentq(lambda x: x - 2, 0, 4))
print(round(minimize_scalar(f).x))
```
**Sortie :** `2.0` puis `2`.
Ces outils sont présentés comme **fonctions de bibliothèque**, sans étudier les algorithmes numériques sous-jacents (réservés à un autre enseignement).
```python
from scipy.optimize import root_scalar, minimize_scalar
f = lambda x: x**3 - x - 2
racine = root_scalar(f, bracket=[1, 2]).root
minimum = minimize_scalar(lambda x: (x-3)**2+2, bounds=(0, 6), method='bounded')
print('Racine :', racine, 'Résidu :', f(racine))
print('Minimum :', minimum.x, minimum.fun)
```
**Résultat de l’exécution :**
```text
Racine : 1.5213797068045676 Résidu : 0.0
Minimum : 3.0 2.0
```
### Matrices et décompositions avec SciPy
```python
import numpy as np
from scipy.linalg import lu, solve_triangular
A = np.array([[4., 2.], [2., 3.]])
P, L, U = lu(A)
print('Décomposition LU vérifiée :', np.allclose(A, P @ L @ U))
T = np.array([[2., 1.], [0., 3.]])
b = np.array([5., 6.])
print('Système triangulaire :', solve_triangular(T, b))
```
**Résultat de l’exécution :**
```text
Décomposition LU vérifiée : True
Système triangulaire : [1.5 2. ]
```
## 9. Fonctions de plusieurs variables et visualisation
Pour $f(x,y)=e^{-(x^2+y^2)}$, `meshgrid` construit toutes les paires $(x_i,y_j)$ et `contourf` affiche les valeurs de la fonction. Il s'agit d'une **évaluation graphique**, pas d'une discrétisation d'équation.
```python
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-2, 2, 100)
y = np.linspace(-2, 2, 100)
X, Y = np.meshgrid(x, y)
Z = np.exp(-(X**2 + Y**2))
plt.contourf(X, Y, Z, levels=20)
plt.colorbar(label='f(x,y)')
plt.xlabel('x'); plt.ylabel('y'); plt.show()
```
**Résultat de l’exécution :**
.png)
Une courbe paramétrée s'écrit $x=x(t)$ et $y=y(t)$. Exemple : $x(t)=\cos(3t)$, $y(t)=\sin(2t)$.
```python
t = np.linspace(0, 2*np.pi, 600)
plt.plot(np.cos(3*t), np.sin(2*t))
plt.axis('equal'); plt.grid(); plt.show()
```
**Résultat de l’exécution :**
![[Courbe paramétrique.png||400]]
### Tracer des fonctions mathématiques
```python
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-3, 3, 400)
plt.plot(x, x**2, label=r'$x^2