%% 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 :** ![figure 02||400](Rotation.png) ### 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 :** ![figure 03||400](f(x,y).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 ) plt.plot(x, x**3 / 3, label=r'$x^3/3 ) plt.axhline(0, color='black', linewidth=0.7) plt.xlabel('x') plt.ylabel('f(x)') plt.title('Comparaison de fonctions') plt.grid(True) plt.legend() plt.show() ``` **Résultat de l’exécution :** ![figure 01||400](Comparaison.png) --- # ➡️ C'est la fin - Cours précèdent: `cours-de-départ` - Prochain cours: [[Exercices - Programmation scientifique|Exercices - Programmation scientifique]] - Page d'accueil de la compétence: [[Programmation Scientifique|Programmation scientifique]] - Page d'accueil du domaine: [[Analyse et Données/Mathématiques/Mathématiques|Mathématiques]] # 🗓️ Historique - Dernière MAJ : `27-octobre-2026` - Rédigé par : [[Antonia JABBOUR]] %% 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 %%