%load_ext watermark
%watermark -v -m -p scipy,numpy,matplotlib,sympy,seaborn -g
CPython 3.5.3 IPython 6.1.0 scipy 0.19.0 numpy 1.12.1 matplotlib 2.0.2 sympy 1.0 seaborn 0.7.1 compiler : GCC 6.3.0 20170118 system : Linux release : 4.10.0-21-generic machine : x86_64 processor : x86_64 CPU cores : 4 interpreter: 64bit Git hash : c4118c39f038f15e7dcde2fa4d6a8df0060ae9e4
import numpy as np
import numpy.linalg as LA
import matplotlib as mpl # inutile
import matplotlib.pyplot as plt
import scipy as sc # pas très utile
Pour avoir de belles figures :
import seaborn as sns
sns.set(context="notebook", style="darkgrid", palette="hls", font="sans-serif", font_scale=1.4)
mpl.rcParams['figure.figsize'] = (19.80, 10.80)
On donne $f_n(t) = \frac{1 - \cos\left(\frac{t}{n}\right)}{t^2(1+t^2)}$.
def f(n, t):
return (1 - np.cos(t / n)) / (t**2 * (1 + t**2))
def y(t):
return 0.5 * np.ones_like(t)
eps = 1e-5
t = np.linspace(0 + eps, np.pi - eps, 1000)
plt.figure()
for n in range(1, 1 + 10):
plt.plot(t, f(n, t), label=r'$f_{%i}(t)$' % n)
plt.plot(t, y(t), label=r'$\frac{1}{2}$')
plt.legend()
plt.title("Courbe demandée")
plt.xlabel(r"$]0, \pi[$")
plt.show()
<matplotlib.figure.Figure at 0x7fcd47ffefd0>
[<matplotlib.lines.Line2D at 0x7fcd47f68748>]
[<matplotlib.lines.Line2D at 0x7fcd47f689e8>]
[<matplotlib.lines.Line2D at 0x7fcd47f73be0>]
[<matplotlib.lines.Line2D at 0x7fcd47f73fd0>]
[<matplotlib.lines.Line2D at 0x7fcd47f818d0>]
[<matplotlib.lines.Line2D at 0x7fcd47f842e8>]
[<matplotlib.lines.Line2D at 0x7fcd47f84cc0>]
[<matplotlib.lines.Line2D at 0x7fcd47f8c6d8>]
[<matplotlib.lines.Line2D at 0x7fcd47f920f0>]
[<matplotlib.lines.Line2D at 0x7fcd47f92ac8>]
[<matplotlib.lines.Line2D at 0x7fcd47f8ebe0>]
<matplotlib.legend.Legend at 0x7fcd47f8ec18>
<matplotlib.text.Text at 0x7fcd47fc35f8>
<matplotlib.text.Text at 0x7fcd4800d0f0>
$f_n(t)$ est bien sûr intégrable sur $[1, +\infty]$, mais c'est moins évident sur $]0, 1]$. La courbe précédente laisse suggérer qu'elle l'est, il faudrait le prouver.
(un développement limité montre qu'en $0$, $f_n(t)$ est prolongeable par continuité en fait)
from scipy.integrate import quad as integral
def u_n(n):
def f_n(t):
return f(n, t)
return integral(f_n, 0, np.inf)[0]
for n in range(1, 1 + 30):
print("- Pour n =", n, "\t u_n =", u_n(n))
- Pour n = 1 u_n = 0.5778636758673165 - Pour n = 2 u_n = 0.1673379686913233 - Pour n = 3 u_n = 0.07832719999247961 - Pour n = 4 u_n = 0.04524016647527334 - Pour n = 5 u_n = 0.0294221986520279 - Pour n = 6 u_n = 0.02065344571531756 - Pour n = 7 u_n = 0.015291770261272906 - Pour n = 8 u_n = 0.011776107184161803 - Pour n = 9 u_n = 0.009346910316513675 - Pour n = 10 u_n = 0.007598599569591147 - Pour n = 11 u_n = 0.006298591120713312 - Pour n = 12 u_n = 0.005305713621776519 - Pour n = 13 u_n = 0.004530420616071271 - Pour n = 14 u_n = 0.003913403332067795 - Pour n = 15 u_n = 0.0034143635712630925 - Pour n = 16 u_n = 0.00300503233562181 - Pour n = 17 u_n = 0.002665127564756733 - Pour n = 18 u_n = 0.0023797953460884366 - Pour n = 19 u_n = 0.002137946272152306 - Pour n = 20 u_n = 0.0019311752431841328 - Pour n = 21 u_n = 0.0017530128954453457 - Pour n = 22 u_n = 0.0015984135117823957 - Pour n = 23 u_n = 0.001463398620721792 - Pour n = 24 u_n = 0.001344791379603347 - Pour n = 25 u_n = 0.0012400485222443885 - Pour n = 26 u_n = 0.0011470784838279937 - Pour n = 27 u_n = 0.001064184893274427 - Pour n = 28 u_n = 0.000989962863081455 - Pour n = 29 u_n = 0.0009232437583422769 - Pour n = 30 u_n = 0.0008630488183994005
Le terme $u_n$ semble tendre vers $0$ pour $n\to +\infty$. On le prouverait avec le théorème de convergence dominée (à faire).
Soit $F(x) = \int_0^{+\infty} \frac{1 - \cos(xt)}{t^2 (1+t^2)} \mathrm{d}t$.
Donc $F$ est bien définie sur $R_+^*$.
Elle est continue par application directe du théorème de continuité sous le signe intégrale.
Elle est prolongeable par continuité en $0$, par $F(0) := 0$ grâce à l'observation précédente : $F(x) \sim \frac{x^2}{2} \int_0^{+\infty} \frac{1}{1+t^2} \mathrm{d}t \to 0$ quand $x\to 0$.
def F(x):
def f_inf(t):
return (1 - np.cos(x * t)) / (t**2 * (1 + t**2))
return integral(f_inf, 0, np.inf)[0]
eps = 1e-4
x = np.linspace(0 + eps, 10, 1000)
plt.figure()
plt.plot(x, np.vectorize(F)(x))
plt.title("$F(x)$ pour $x = 0 .. 10$")
plt.show()
<matplotlib.figure.Figure at 0x7fcd4243d6d8>
/usr/local/lib/python3.5/dist-packages/scipy/integrate/quadpack.py:364: IntegrationWarning: The maximum number of subdivisions (50) has been achieved. If increasing the limit yields no improvement it is advised to analyze the integrand in order to determine the difficulties. If the position of a local difficulty can be determined (singularity, discontinuity) one will probably gain from splitting up the interval and calling the integrator on the subranges. Perhaps a special-purpose integrator should be used. warnings.warn(msg, IntegrationWarning)
[<matplotlib.lines.Line2D at 0x7fcd2ccf3a90>]
<matplotlib.text.Text at 0x7fcd2ccd7470>
On constate sur la figure que $F$ est bien prolongeable par continuité en $0$.
On montrerait aussi que $F$ est de classe $\mathcal{C}^1$ facilement, par application directe du théorème de dérivation généralisée sous le signe intégral.
Soit $(P_n)_{n\geq 0}$ une suite de polynômes définis par $P_0 = 1$, $P_1 = 2X$ et $P_{n+1} = 2 X P_n - P_{n-1}$. Calculons $P_2,\dots,P_8$.
On pourrait tout faire avec des listes gérées manuellement, mais c'est assez compliqué.
Il vaut mieux aller vite, en utilisant le module numpy.polynomial.
# Ce morceau est juste là pour avoir un joli rendu
def Polynomial_to_LaTeX(p):
""" Small function to print nicely the polynomial p as we write it in maths, in LaTeX code.
- Source: https://nbviewer.jupyter.org/github/Naereen/notebooks/blob/master/Demonstration%20of%20numpy.polynomial.Polynomial%20and%20nice%20display%20with%20LaTeX%20and%20MathJax%20%28python3%29.ipynb
"""
coefs = p.coef # List of coefficient, sorted by increasing degrees
res = "" # The resulting string
for i, a in enumerate(coefs):
if int(a) == a: # Remove the trailing .0
a = int(a)
if i == 0: # First coefficient, no need for X
if a > 0:
res += "{a} + ".format(a=a)
elif a < 0: # Negative a is printed like (a)
res += "({a}) + ".format(a=a)
# a = 0 is not displayed
elif i == 1: # Second coefficient, only X and not X**i
if a == 1: # a = 1 does not need to be displayed
res += "X + "
elif a > 0:
res += "{a} \;X + ".format(a=a)
elif a < 0:
res += "({a}) \;X + ".format(a=a)
else:
if a == 1:
# A special care needs to be addressed to put the exponent in {..} in LaTeX
res += "X^{i} + ".format(i="{%d}" % i)
elif a > 0:
res += "{a} \;X^{i} + ".format(a=a, i="{%d}" % i)
elif a < 0:
res += "({a}) \;X^{i} + ".format(a=a, i="{%d}" % i)
if res == "":
res = "0000"
return "$" + res[:-3] + "$"
def setup_prrint():
ip = get_ipython()
latex_formatter = ip.display_formatter.formatters['text/latex']
latex_formatter.for_type_by_name('numpy.polynomial.polynomial',
'Polynomial', Polynomial_to_LaTeX)
setup_prrint()
Je recommande d'importer numpy.polynomial.Polynomial
et de l'appeller P
.
Définir directement le monôme $X$ comme P([0, 1])
, donné par la liste de ses coefficients $[a_k]_{0 \leq k \leq \delta(X)} = [0, 1]$.
from numpy.polynomial import Polynomial as P
X = P([0, 1])
X
Ensuite, on peut rapidement écrire une fonction, qui donne $P_n$ pour un $n \geq 0$. Pas besoin d'être malin, on recalcule tout dans la fonction.
Pnm1
signifie $P_{n - 1}$Pnext
signifie $P_{n + 1}$def P_n(n):
P0 = P([1])
P1 = P([0, 2])
Pnm1, Pn = P0, P1
for i in range(n):
Pnext = (2 * X * Pn) - Pnm1
Pnm1, Pn = Pn, Pnext
return Pnm1
for n in range(0, 1 + 8):
print("Pour n =", n, "P_n =")
P_n(n)
Pour n = 0 P_n =
Pour n = 1 P_n =
Pour n = 2 P_n =
Pour n = 3 P_n =
Pour n = 4 P_n =
Pour n = 5 P_n =
Pour n = 6 P_n =
Pour n = 7 P_n =
Pour n = 8 P_n =
Premières observations :
Ces trois points se montrent assez rapidement par récurrence simple, à partir de $P_0,P_1$ et la relation de récurrence définissant $P_n$.
On vérifie mathématiquement que $\langle P, Q \rangle := \frac{2}{\pi} \int_{-1}^{1} \sqrt{1-t^2} P(t) Q(t) \mathrm{d}t$ est un produit scalaire pour les polynômes réels. (il est évidemment bien défini puisque la racine carrée existe, et que les fonctions intégrées sont de continues sur $[-1,1]$, symétrique, positif si $P=Q$, et il est défini parce que $P^2(t) \geq 0$).
Calculons $\langle P_i, P_j \rangle$ pour $0 \leq i,j \leq 8$.
L'intégration est faite numériquement, avec scipy.integrate.quad
.
from scipy.integrate import quad
def produit_scalaire(P, Q):
def f(t):
return np.sqrt(1 - t**2) * P(t) * Q(t)
return (2 / np.pi) * quad(f, -1, 1)[0]
# on calcule qu'une seule fois
P_n_s = [P_n(n) for n in range(0, 1 + 8)]
for i in range(1, 1 + 8):
for j in range(i, 1 + 8):
Pi, Pj = P_n_s[i], P_n_s[j]
ps = np.round(produit_scalaire(Pi, Pj), 8)
print("< P_{}, P_{} > = {:.3g}".format(i, j, ps))
< P_1, P_1 > = 1 < P_1, P_2 > = 0 < P_1, P_3 > = -0 < P_1, P_4 > = 0 < P_1, P_5 > = -0 < P_1, P_6 > = 0 < P_1, P_7 > = -0 < P_1, P_8 > = 0 < P_2, P_2 > = 1 < P_2, P_3 > = 0 < P_2, P_4 > = -0 < P_2, P_5 > = 0 < P_2, P_6 > = -0 < P_2, P_7 > = 0 < P_2, P_8 > = -0 < P_3, P_3 > = 1 < P_3, P_4 > = 0 < P_3, P_5 > = -0 < P_3, P_6 > = 0 < P_3, P_7 > = -0 < P_3, P_8 > = 0 < P_4, P_4 > = 1 < P_4, P_5 > = 0 < P_4, P_6 > = -0 < P_4, P_7 > = 0 < P_4, P_8 > = -0 < P_5, P_5 > = 1 < P_5, P_6 > = 0 < P_5, P_7 > = -0 < P_5, P_8 > = 0 < P_6, P_6 > = 1 < P_6, P_7 > = 0 < P_6, P_8 > = -0 < P_7, P_7 > = 1 < P_7, P_8 > = 0 < P_8, P_8 > = 1
produits_scalaires = np.zeros((8, 8))
for i in range(1, 1 + 8):
for j in range(i, 1 + 8):
Pi, Pj = P_n_s[i], P_n_s[j]
produits_scalaires[i - 1, j - 1] = np.round(produit_scalaire(Pi, Pj), 8)
produits_scalaires.astype(int)
array([[1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1]])
La famille $(P_i)_{0 \leq i \leq 8}\;$ est orthogonale.
(les -0
sont des 0
, la différence vient des erreurs d'arrondis).
Soit $\Phi(P) = 3XP' - (1-X^2)P''$ (erreur dans l'énoncé, le deuxième terme est évidemment $P''$ et non $P'$). Elle conserve (ou diminue) le degré de $P$.
def Phi(P):
return 3 * X * P.deriv() - (1 - X**2) * P.deriv(2)
On calcule sa matrice de passage, dans la base $(P_i)_{1\leq i \leq 8}$ :
# on calcule qu'une seule fois
P_n_s = [P_n(n) for n in range(0, 1 + 8)]
matrice_Phi = [
[
np.round(produit_scalaire(Phi(P_n_s[i]), P_n_s[j]), 8)
for i in range(1, 1 + 8)
] for j in range(1, 1 + 8)
]
matrice_Phi = np.array(matrice_Phi, dtype=int)
matrice_Phi.shape
matrice_Phi
(8, 8)
array([[ 3, 0, 0, 0, 0, 0, 0, 0], [ 0, 8, 0, 0, 0, 0, 0, 0], [ 0, 0, 15, 0, 0, 0, 0, 0], [ 0, 0, 0, 24, 0, 0, 0, 0], [ 0, 0, 0, 0, 35, 0, 0, 0], [ 0, 0, 0, 0, 0, 48, 0, 0], [ 0, 0, 0, 0, 0, 0, 63, 0], [ 0, 0, 0, 0, 0, 0, 0, 80]])
Elle est diagonale ! Et trivialement inversible !
from scipy.linalg import det
det(matrice_Phi)
73156608000.0
Cette matrice est inversible, donc dans la base $(P_i)_{1\leq i \leq 8}$, l'application linéaire $\Phi$ est une bijection.
On peut même dire plus : en renormalisant les $P_i$, on peut faire de $\Phi$ l'identité...
P_n_s_normalises = np.asarray(P_n_s[1:]) / np.sqrt(matrice_Phi.diagonal())
matrice_Phi_normalise = [
[
np.round(produit_scalaire(Phi(P_n_s_normalises[i - 1]), P_n_s_normalises[j - 1]), 8)
for i in range(1, 1 + 8)
] for j in range(1, 1 + 8)
]
matrice_Phi_normalise = np.array(matrice_Phi_normalise)
matrice_Phi_normalise.astype(int)
array([[1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1]])
On peut utiliser ce fait pour montrer, par deux intégrations par parties, le résultat annoncé sur l'orthogonalité de la famille $(P_i)_{1\leq i \leq 8}\;$.
On étudie le comportement d'une particule évoluant sur 4 états, avec certaines probabilités :
On fixe la constante $p = \frac12$ dès maintenant, on définit la matrice de transition $A$, telle que définie un peu après dans l'exercice.
Les états sont représentés par [0, 1, 2, 3]
plutôt que $A_0, A_1, A_2, A_3$.
p = 0.5
A = np.array([
[1, 0, 0, 0],
[p, 0, 1-p, 0],
[0, p, 0, 1-p],
[0, 0, 0, 1]
])
etats = [0, 1, 2, 3]
import numpy.random as rd
Une transition se fait en choisissant un état $x_{n+1}$ parmi $\{0, 1, 2, 3\}$, avec probabilité $\mathbb{P}(x_{n+1} = k) = A_{x_n, k}$.
La fonction numpy.random.choice
fait ça directement.
def une_transition(xn):
return rd.choice(etats, p = A[xn])
une_transition(0)
une_transition(1)
une_transition(2)
une_transition(3)
0
0
3
3
On peut écrire la fonction à la main, comme :
def une_transition_longue(xn):
if xn == 0 or xn == 3:
return xn
elif xn == 1:
if rd.random() < p:
return 0 # avec probabilité p
else:
return 2 # avec probabilité 1-p
elif xn == 2:
if rd.random() < p:
return 1
else:
return 3
une_transition_longue(0)
une_transition_longue(1)
une_transition_longue(2)
une_transition_longue(3)
0
2
1
3
Faire plusieurs transitions se fait juste en appliquant la même fonction $n$ fois.
def n_transitions(n, x0):
x = x0
for i in range(n):
x = une_transition(x)
return x
n_transitions(10, 0)
n_transitions(10, 1)
n_transitions(10, 2)
n_transitions(10, 3)
0
0
0
3
Faisons $N=1000$ répétitions de cette expérience, à l'horizon disons $n=100$.
n = 100
N = 1000
def histogramme(n, N, x0):
observations = np.zeros(len(etats))
for experience in range(N):
obs = n_transitions(n, x0)
observations[obs] += 1
plt.bar(etats, observations)
plt.show()
histogramme(n, N, 0)
histogramme(n, N, 1)
histogramme(n, N, 2)
histogramme(n, N, 3)
Mathématiquement, sur papier on calcule le polynôme caractéristique de $A$, et on vérifie qu'il est scindé ssi $p \neq 0, 1$ (mais pas à racine simple).
Pour diagonaliser, on utile le module numpy.linalg
, et la fonction numpy.linalg.eig
.
from numpy import linalg as LA
A = A.T
A
array([[ 1. , 0.5, 0. , 0. ], [ 0. , 0. , 0.5, 0. ], [ 0. , 0.5, 0. , 0. ], [ 0. , 0. , 0.5, 1. ]])
spectre, matricePassage = LA.eig(A)
spectre
matricePassage
array([ 1. , 1. , 0.5, -0.5])
array([[ 1. , 0. , -0.5 , -0.2236068 ], [ 0. , 0. , 0.5 , 0.67082039], [ 0. , 0. , 0.5 , -0.67082039], [ 0. , 1. , -0.5 , 0.2236068 ]])
Ici, on vérifie que le spectre contient deux fois la valeur propre $1$, qui vient des deux puits $A_0,A_3$, et deux valeurs symétriques.
On peut vérifier que $A = P \Lambda P^{-1}$
Lambda = np.diag(spectre)
matricePassageinv = LA.inv(matricePassage)
# avec Python >= 3.6
matricePassage @ Lambda @ matricePassageinv
# avant 3.6
matricePassage.dot(Lambda.dot(matricePassageinv))
array([[ 1.00000000e+00, 5.00000000e-01, -6.14248029e-17, 0.00000000e+00], [ 0.00000000e+00, -4.08433854e-17, 5.00000000e-01, 0.00000000e+00], [ 0.00000000e+00, 5.00000000e-01, 1.34656917e-16, 0.00000000e+00], [ 0.00000000e+00, 5.78726134e-17, 5.00000000e-01, 1.00000000e+00]])
array([[ 1.00000000e+00, 5.00000000e-01, -9.74554924e-17, 0.00000000e+00], [ 0.00000000e+00, -4.49809423e-17, 5.00000000e-01, 0.00000000e+00], [ 0.00000000e+00, 5.00000000e-01, 1.30519360e-16, 0.00000000e+00], [ 0.00000000e+00, 4.95974995e-17, 5.00000000e-01, 1.00000000e+00]])
Sans erreur d'arrondis, ça donne :
np.round(matricePassage @ Lambda @ matricePassageinv, 3)
np.round(matricePassage.dot(Lambda.dot(matricePassageinv)), 3)
np.all(np.round(matricePassage @ Lambda @ matricePassageinv, 3) == A)
array([[ 1. , 0.5, -0. , 0. ], [ 0. , -0. , 0.5, 0. ], [ 0. , 0.5, 0. , 0. ], [ 0. , 0. , 0.5, 1. ]])
array([[ 1. , 0.5, -0. , 0. ], [ 0. , -0. , 0.5, 0. ], [ 0. , 0.5, 0. , 0. ], [ 0. , 0. , 0.5, 1. ]])
True
On peut ensuite calculer $\lim_{n\to\infty} X_n$ en calculant $P \Lambda' P^{-1} X_0$ si $\Lambda' := \lim_{n\to\infty} \Lambda^n = \mathrm{Diag}(\lim_{n\to\infty} \lambda_i^n)$ qui existe bien puisque $\mathrm{Sp}(A) = \{1, \pm\sqrt{p(1-p)}\} \subset [-1,1]$.
def limite_inf(t):
if t <= -1:
raise ValueError("Pas de limite")
elif -1 < t < 1:
return 0
elif t == 1:
return 1
else:
return np.inf
LambdaInf = np.diag([limite_inf(lmbda) for lmbda in spectre])
LambdaInf
array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]])
for x0 in etats:
X0 = np.zeros(len(etats))
X0[x0] = 1
print("Pour X0 =", X0)
Xinf = (matricePassage @ LambdaInf @ matricePassageinv) @ X0
print(" => limite Xn pour n -> oo =", Xinf)
Pour X0 = [ 1. 0. 0. 0.] => limite Xn pour n -> oo = [ 1. 0. 0. 0.] Pour X0 = [ 0. 1. 0. 0.] => limite Xn pour n -> oo = [ 0.66666667 0. 0. 0.33333333] Pour X0 = [ 0. 0. 1. 0.] => limite Xn pour n -> oo = [ 0.33333333 0. 0. 0.66666667] Pour X0 = [ 0. 0. 0. 1.] => limite Xn pour n -> oo = [ 0. 0. 0. 1.]
Ça correspond exactement aux histogrammes obtenus plus haut. Peu importe l'état initial, la particule finira dans un des deux puits. (C'est ce qu'on appelle des états absorbants)
Se préparer aux oraux de "maths avec Python" (maths 2) du concours Centrale Supélec peut être utile.
Après les écrits et la fin de l'année, pour ceux qui seront admissibles à Centrale-Supélec, ils vous restera les oraux (le concours Centrale-Supélec a un oral d'informatique, et un peu d'algorithmique et de Python peuvent en théorie être demandés à chaque oral de maths et de SI).
Je vous invite à lire cette page avec attention, et à jeter un œil aux documents mis à disposition :
Pour réviser : voir ce tutoriel Matplotlib (en anglais), ce tutoriel Numpy (en anglais). Ainsi que tous les TP, TD et DS en Python que j'ai donné et corrigé au Lycée Lakanal (Sceaux, 92) en 2015-2016 !
Ces 5 sujets sont corrigés, et nous les avons tous traité en classe durant les deux TP de révisions pour les oraux (10 et 11 juin).
Ce document est distribué sous licence libre (MIT), comme les autres notebooks que j'ai écrit depuis 2015.