---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Chapitre 2 — La couche physique

La couche physique est la fondation de toute communication réseau. Elle définit comment les bits sont transformés en signaux physiques et transmis sur un support. Sans elle, aucune des couches supérieures n'existerait.

```{code-cell} python
:tags: [hide-input]
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
import pandas as pd
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 120,
    "font.family": "DejaVu Sans",
    "axes.spines.top": False,
    "axes.spines.right": False,
})
```

## Signaux analogiques et numériques

### Signal analogique

Un signal analogique varie de façon **continue** dans le temps. La voix humaine, la radio FM, les signaux sur un câble téléphonique traditionnel sont analogiques. Un signal sinusoïdal est caractérisé par :

- **Amplitude** A : hauteur du signal (en volts)
- **Fréquence** f : nombre d'oscillations par seconde (en Hz)
- **Phase** φ : décalage temporel (en radians)

$$s(t) = A \cdot \sin(2\pi f t + \varphi)$$

### Signal numérique

Un signal numérique ne prend que des valeurs **discrètes** (typiquement 0 et 1). Les ordinateurs et réseaux modernes travaillent en numérique. La conversion entre analogique et numérique se fait par **échantillonnage** (ADC) et **reconstruction** (DAC).

```{code-cell} python
:tags: [hide-input]
t = np.linspace(0, 0.004, 2000)
f = 1000  # 1 kHz

fig, axes = plt.subplots(2, 2, figsize=(13, 7))
fig.suptitle("Signaux analogiques et numériques", fontsize=14, fontweight="bold")

# Signal sinusoïdal pur
ax = axes[0, 0]
signal_sin = np.sin(2 * np.pi * f * t)
ax.plot(t * 1000, signal_sin, color="#e74c3c", linewidth=1.8)
ax.set_title("Signal sinusoïdal (1 kHz)")
ax.set_xlabel("Temps (ms)")
ax.set_ylabel("Amplitude (V)")
ax.set_ylim(-1.4, 1.4)

# Signal avec harmoniques (signal carré approché)
ax = axes[0, 1]
signal_carre = sum(np.sin(2 * np.pi * (2*k+1) * f * t) / (2*k+1) for k in range(10))
signal_carre /= np.max(np.abs(signal_carre))
ax.plot(t * 1000, signal_carre, color="#e67e22", linewidth=1.8)
ax.set_title("Signal carré (approximé par harmoniques)")
ax.set_xlabel("Temps (ms)")
ax.set_ylabel("Amplitude normalisée")

# Signal numérique NRZ
ax = axes[1, 0]
bits = [1, 0, 1, 1, 0, 0, 1, 0]
t_num = np.linspace(0, len(bits), len(bits) * 200)
signal_nrz = np.array([bits[min(int(x), len(bits)-1)] * 2 - 1 for x in t_num])
ax.step(t_num, signal_nrz, where="post", color="#2980b9", linewidth=2)
ax.set_title("Signal numérique NRZ")
ax.set_xlabel("Temps (symboles)")
ax.set_ylabel("Niveau (V)")
ax.set_ylim(-1.5, 1.5)
ax.set_yticks([-1, 1])
ax.set_yticklabels(["0 (−V)", "1 (+V)"])
for i, b in enumerate(bits):
    ax.text(i + 0.5, 1.25, str(b), ha="center", va="center",
            fontsize=11, fontweight="bold", color="#2980b9")

# Spectre de fréquences
ax = axes[1, 1]
freqs = np.fft.rfftfreq(len(t), t[1]-t[0])
spectre = np.abs(np.fft.rfft(signal_sin))
ax.plot(freqs / 1000, spectre, color="#27ae60", linewidth=1.5)
ax.set_title("Spectre fréquentiel du signal sinusoïdal")
ax.set_xlabel("Fréquence (kHz)")
ax.set_ylabel("Amplitude")
ax.set_xlim(0, 3)

plt.tight_layout()
plt.show()
```

### Bande passante

La **bande passante** (bandwidth) désigne l'intervalle de fréquences qu'un canal peut transmettre sans atténuation excessive. Elle est mesurée en **Hz**. À ne pas confondre avec le débit (mesuré en bits/s), bien que les deux soient liés.

```{admonition} Bande passante ≠ Débit
:class: note
La bande passante en Hz est une propriété physique du canal. Le débit en bits/s est la quantité de données transmises par seconde. La relation entre les deux est donnée par les théorèmes de Nyquist et Shannon.
```

---

## Théorèmes de Nyquist et Shannon

### Théorème de Nyquist (canal sans bruit)

Pour un canal de bande passante B et M niveaux de signal distincts :

$$D_{max} = 2B \log_2 M \quad \text{(bits/s)}$$

**Exemple** : Un câble téléphonique de 3 kHz avec 4 niveaux → $D_{max} = 2 \times 3000 \times \log_2 4 = 12\,000$ bits/s.

### Théorème de Shannon-Hartley (canal bruité)

Pour un canal de bande passante B et un rapport signal/bruit (SNR) :

$$C = B \log_2\!\left(1 + \frac{S}{N}\right) \quad \text{(bits/s)}$$

C est la **capacité de Shannon** — limite théorique absolue du débit, indépendamment du codage.

```{code-cell} python
import math

def capacite_shannon(bande_hz: float, snr_db: float) -> float:
    """Calcule la capacité de Shannon (bits/s)."""
    snr_lineaire = 10 ** (snr_db / 10)
    return bande_hz * math.log2(1 + snr_lineaire)

def debit_nyquist(bande_hz: float, niveaux: int) -> float:
    """Calcule le débit maximal de Nyquist (bits/s) sans bruit."""
    return 2 * bande_hz * math.log2(niveaux)

print("=== Théorème de Shannon ===")
exemples = [
    ("Téléphone RTC",        3_400,  30),
    ("ADSL2+",             2_208_000,  40),
    ("Wi-Fi 802.11n (40 MHz)", 40_000_000,  25),
    ("Fibre monomode (1 THz)", 1_000_000_000_000, 30),
]
for nom, bande, snr in exemples:
    c = capacite_shannon(bande, snr)
    print(f"  {nom:<35} B={bande:>15,} Hz  SNR={snr:>3} dB  → C≈{c:>15,.0f} bits/s")

print("\n=== Théorème de Nyquist ===")
for bande, niveaux, desc in [(3400, 2, "Téléphone, binaire"),
                               (3400, 4, "Téléphone, 4 niveaux"),
                               (40e6, 256, "Wi-Fi, 256-QAM")]:
    d = debit_nyquist(bande, niveaux)
    print(f"  {desc:<30} B={int(bande):>12,} Hz  M={niveaux:>4}  → D≈{d:>15,.0f} bits/s")
```

```{code-cell} python
:tags: [hide-input]
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Capacité de Shannon en fonction du SNR
ax = axes[0]
snr_range = np.linspace(0, 40, 200)
for bande, label, color in [(3400, "Téléphone 3.4 kHz", "#e74c3c"),
                              (100e3, "DSL 100 kHz",      "#e67e22"),
                              (20e6,  "Wi-Fi 20 MHz",     "#27ae60"),
                              (160e6, "Wi-Fi 160 MHz",    "#2980b9")]:
    capacites = [capacite_shannon(bande, s) / 1e6 for s in snr_range]
    ax.plot(snr_range, capacites, label=label, linewidth=2)
ax.set_xlabel("SNR (dB)")
ax.set_ylabel("Capacité (Mbit/s)")
ax.set_title("Capacité de Shannon vs SNR")
ax.legend(fontsize=8)
ax.grid(True, alpha=0.4)

# Débit Nyquist en fonction du nombre de niveaux
ax2 = axes[1]
niveaux_range = [2**n for n in range(1, 11)]  # 2, 4, 8, ..., 1024
for bande, label, color in [(3400, "3.4 kHz", "#e74c3c"),
                              (1e6,  "1 MHz",   "#27ae60"),
                              (40e6, "40 MHz",  "#2980b9")]:
    debits = [debit_nyquist(bande, m) / 1e6 for m in niveaux_range]
    ax2.semilogx(niveaux_range, debits, marker="o", label=label, linewidth=2, markersize=5)
ax2.set_xlabel("Nombre de niveaux M (échelle log)")
ax2.set_ylabel("Débit max Nyquist (Mbit/s)")
ax2.set_title("Débit Nyquist vs niveaux de signal")
ax2.legend(fontsize=9)
ax2.grid(True, alpha=0.4)

plt.tight_layout()
plt.show()
```

---

## Médias de transmission

### Paire torsadée

La **paire torsadée** est le média le plus répandu dans les réseaux locaux. Elle est constituée de fils de cuivre torsadés par paires pour réduire les interférences électromagnétiques (diaphonie).

| Catégorie | Débit max | Portée max | Usage |
|-----------|-----------|------------|-------|
| Cat5e | 1 Gbit/s | 100 m | Ethernet 1000BASE-T |
| Cat6 | 10 Gbit/s | 55 m | Ethernet 10GBASE-T |
| Cat6a | 10 Gbit/s | 100 m | Data centers |
| Cat8 | 40 Gbit/s | 30 m | Data centers haute densité |

### Fibre optique

La **fibre optique** transmet la lumière dans un fil de verre ou de plastique. Elle est immune aux interférences électromagnétiques et offre des distances bien supérieures.

| Type | Cœur | Usage | Portée |
|------|------|-------|--------|
| Monomode (SMF) | 8–10 µm | WAN, longue distance | Jusqu'à 100 km sans répéteur |
| Multimode OM3 | 50 µm | Data centers | 300 m à 10 Gbit/s |
| Multimode OM4 | 50 µm | Data centers | 550 m à 10 Gbit/s |

La fibre monomode n'autorise qu'un seul mode de propagation de la lumière (pas de dispersion modale), ce qui permet des distances et des débits bien supérieurs.

### Wi-Fi (IEEE 802.11)

Le Wi-Fi utilise les bandes de fréquences 2.4 GHz, 5 GHz et 6 GHz. Chaque génération améliore le débit grâce à des canaux plus larges et des modulations plus efficaces.

| Norme | Fréquence | Débit max théorique | Technologie |
|-------|-----------|---------------------|-------------|
| 802.11g | 2.4 GHz | 54 Mbit/s | OFDM |
| 802.11n (Wi-Fi 4) | 2.4/5 GHz | 600 Mbit/s | MIMO, canaux 40 MHz |
| 802.11ac (Wi-Fi 5) | 5 GHz | 6.9 Gbit/s | MU-MIMO, 256-QAM |
| 802.11ax (Wi-Fi 6) | 2.4/5/6 GHz | 9.6 Gbit/s | OFDMA, 1024-QAM |

---

## Codages en ligne

Le **codage en ligne** (line coding) définit comment les bits 0 et 1 sont représentés électriquement sur le support. Un bon codage doit permettre la synchronisation, éviter les longues séquences de même symbole et faciliter la détection d'erreurs.

### NRZ (Non-Return-to-Zero)

- **NRZ-L** : 1 = niveau haut, 0 = niveau bas. Simple, mais perd la synchronisation sur de longues séquences identiques.
- **NRZ-I** : Transition sur 1, pas de transition sur 0. Mieux pour la synchro, mais toujours problème avec les longues séquences de 0.

### Manchester

Chaque bit contient une **transition au milieu de la période** : 1 = transition bas→haut, 0 = transition haut→bas. Synchronisation parfaite, mais la bande passante nécessaire est doublée. Utilisé dans l'Ethernet 10BASE-T.

### 4B5B

Groupe de 4 bits encodés en 5 bits, garantissant un maximum de deux 0 consécutifs. Suivi d'un codage NRZ-I. Utilisé dans Fast Ethernet (100BASE-TX) et FDDI.

```{code-cell} python
:tags: [hide-input]
bits = [1, 0, 1, 1, 0, 0, 0, 1, 0, 1]

def nrz_signal(bits, samples=50):
    """Génère un signal NRZ-L."""
    signal = []
    for b in bits:
        signal.extend([1.0 if b == 1 else -1.0] * samples)
    return np.array(signal)

def manchester_signal(bits, samples=50):
    """Génère un signal Manchester (IEEE 802.3 : 0 = haut→bas, 1 = bas→haut)."""
    signal = []
    for b in bits:
        half = samples // 2
        if b == 1:
            signal.extend([-1.0] * half + [1.0] * (samples - half))
        else:
            signal.extend([1.0] * half + [-1.0] * (samples - half))
    return np.array(signal)

def nrzi_signal(bits, samples=50):
    """Génère un signal NRZ-I (transition sur 1)."""
    signal = []
    niveau = -1.0
    for b in bits:
        if b == 1:
            niveau = -niveau
        signal.extend([niveau] * samples)
    return np.array(signal)

samples = 60
t = np.linspace(0, len(bits), len(bits) * samples)

sig_nrz     = nrz_signal(bits, samples)
sig_nrzi    = nrzi_signal(bits, samples)
sig_manch   = manchester_signal(bits, samples)

fig, axes = plt.subplots(3, 1, figsize=(13, 8), sharex=True)
fig.suptitle("Codages en ligne pour la séquence : " + "".join(map(str, bits)),
             fontsize=13, fontweight="bold")

for ax, signal, title, color in zip(
        axes,
        [sig_nrz, sig_nrzi, sig_manch],
        ["NRZ-L (Non-Return-to-Zero Level)",
         "NRZ-I (Non-Return-to-Zero Inverted)",
         "Manchester (IEEE 802.3)"],
        ["#e74c3c", "#2980b9", "#27ae60"]):
    ax.step(t, signal, where="post", color=color, linewidth=2)
    ax.set_ylim(-1.6, 1.8)
    ax.set_yticks([-1, 1])
    ax.set_yticklabels(["0 (−V)", "1 (+V)"])
    ax.set_ylabel("Niveau")
    ax.set_title(title, fontsize=10, fontweight="bold", color=color)
    ax.axhline(0, color="#cccccc", linewidth=0.8, linestyle="--")
    # Affiche les bits
    for i, b in enumerate(bits):
        ax.text(i + 0.5, 1.5, str(b), ha="center", va="center",
                fontsize=11, fontweight="bold", color=color)
    # Lignes verticales de délimitation
    for i in range(len(bits) + 1):
        ax.axvline(i, color="#cccccc", linewidth=0.8, linestyle=":")

axes[-1].set_xlabel("Temps (symboles)")
plt.tight_layout()
plt.show()
```

---

## Modulation

La **modulation** permet de transposer un signal numérique sur une porteuse analogique pour la transmission sur des supports radio ou optiques.

### Modulations de base

**AM (Amplitude Modulation)** : L'amplitude de la porteuse varie selon le signal.

**FM (Frequency Modulation)** : La fréquence varie selon le signal. Plus robuste aux perturbations d'amplitude.

**PM (Phase Modulation)** : La phase de la porteuse varie.

### QAM (Quadrature Amplitude Modulation)

La QAM combine modulation d'amplitude et de phase. Un symbole QAM encode plusieurs bits simultanément en choisissant un point dans un espace 2D (constellation). En **16-QAM**, chaque symbole encode 4 bits (16 points). En **256-QAM**, 8 bits par symbole.

### OFDM (Orthogonal Frequency Division Multiplexing)

Utilisé dans Wi-Fi (802.11a/g/n/ac/ax), 4G (LTE) et 5G, OFDM divise le canal en de nombreuses **sous-porteuses orthogonales** de faible débit. Avantages :
- Résistance aux évanouissements sélectifs en fréquence
- Bonne efficacité spectrale
- Facile à égaliser avec une FFT

```{code-cell} python
:tags: [hide-input]
fig, axes = plt.subplots(2, 2, figsize=(13, 10))
fig.suptitle("Modulations numériques", fontsize=14, fontweight="bold")

t_mod = np.linspace(0, 2e-6, 2000)
fc = 5e6  # porteuse 5 MHz

# ---- ASK (Amplitude Shift Keying) ----
ax = axes[0, 0]
bits_demo = [1, 0, 1, 1, 0]
t_ask = np.linspace(0, len(bits_demo), len(bits_demo) * 200)
porteuse = np.sin(2 * np.pi * 5 * t_ask)  # 5 cycles par symbole
amp = np.array([bits_demo[min(int(x), len(bits_demo)-1)] for x in t_ask], dtype=float)
ax.plot(t_ask, amp * porteuse, color="#e74c3c", linewidth=1.2)
ax.set_title("ASK — Amplitude Shift Keying")
ax.set_xlabel("Temps (symboles)")
ax.set_ylabel("Signal")
for i, b in enumerate(bits_demo):
    ax.text(i + 0.5, 1.2, str(b), ha="center", fontsize=11, fontweight="bold", color="#e74c3c")

# ---- FSK (Frequency Shift Keying) ----
ax = axes[0, 1]
signal_fsk = []
for b in bits_demo:
    freq = 8 if b == 1 else 3  # fréquences différentes selon le bit
    t_sym = np.linspace(0, 1, 200)
    signal_fsk.extend(np.sin(2 * np.pi * freq * t_sym))
t_fsk = np.linspace(0, len(bits_demo), len(bits_demo) * 200)
ax.plot(t_fsk, signal_fsk, color="#2980b9", linewidth=1.2)
ax.set_title("FSK — Frequency Shift Keying")
ax.set_xlabel("Temps (symboles)")
ax.set_ylabel("Signal")
for i, b in enumerate(bits_demo):
    ax.text(i + 0.5, 1.2, str(b), ha="center", fontsize=11, fontweight="bold", color="#2980b9")

# ---- Constellation 16-QAM ----
ax = axes[1, 0]
n_points = 16
gray_code = []
for i in range(4):
    for j in range(4):
        # Mappage Gray 2D
        gi = i ^ (i >> 1)
        gj = j ^ (j >> 1)
        gray_code.append((gi, gj))

levels = [-3, -1, 1, 3]
for code, (ci, cj) in zip(range(16), [(levels[g[0]], levels[g[1]]) for g in gray_code]):
    ax.scatter(ci, cj, color="#e74c3c", s=80, zorder=3)
    ax.text(ci + 0.15, cj + 0.15,
            f"{code:04b}", fontsize=7, color="#333333")

ax.axhline(0, color="#cccccc", linewidth=0.8)
ax.axvline(0, color="#cccccc", linewidth=0.8)
ax.set_xlim(-4.5, 4.5)
ax.set_ylim(-4.5, 4.5)
ax.set_title("Constellation 16-QAM (4 bits/symbole)")
ax.set_xlabel("Composante I (en phase)")
ax.set_ylabel("Composante Q (quadrature)")
ax.set_aspect("equal")
ax.grid(True, alpha=0.3)

# ---- Constellation 64-QAM ----
ax = axes[1, 1]
levels8 = np.arange(-7, 8, 2)  # -7, -5, -3, -1, 1, 3, 5, 7
for i in levels8:
    for j in levels8:
        ax.scatter(i, j, color="#27ae60", s=30, zorder=3, alpha=0.8)

ax.axhline(0, color="#cccccc", linewidth=0.8)
ax.axvline(0, color="#cccccc", linewidth=0.8)
ax.set_xlim(-9, 9)
ax.set_ylim(-9, 9)
ax.set_title("Constellation 64-QAM (6 bits/symbole)")
ax.set_xlabel("Composante I")
ax.set_ylabel("Composante Q")
ax.set_aspect("equal")
ax.grid(True, alpha=0.3)
# Cercles de bruit simulé
rng = np.random.default_rng(42)
for i in levels8:
    for j in levels8:
        bruit = rng.normal(0, 0.3, (20, 2))
        ax.scatter(i + bruit[:, 0], j + bruit[:, 1],
                   color="#27ae60", s=5, alpha=0.15, zorder=2)

plt.tight_layout()
plt.show()
```

### OFDM visualisé

```{code-cell} python
:tags: [hide-input]
fig, axes = plt.subplots(2, 1, figsize=(13, 8))
fig.suptitle("OFDM — Multiplexage par répartition en fréquences orthogonales", fontsize=13, fontweight="bold")

t_ofdm = np.linspace(0, 1, 1000)
n_subcarriers = 8
freqs_sub = np.arange(1, n_subcarriers + 1)
couleurs = plt.cm.tab10(np.linspace(0, 1, n_subcarriers))

# Sous-porteuses individuelles
ax = axes[0]
signal_total = np.zeros_like(t_ofdm)
for f_sub, color in zip(freqs_sub, couleurs):
    amp = np.random.choice([-1, 1])  # BPSK simple
    s = amp * np.cos(2 * np.pi * f_sub * t_ofdm)
    ax.plot(t_ofdm, s + f_sub * 2.5, color=color, linewidth=1.2, alpha=0.8)
    signal_total += s
ax.set_title(f"8 sous-porteuses OFDM individuelles (f₁ à f₈)")
ax.set_ylabel("Fréquence (offset)")
ax.set_xlabel("Temps")
ax.set_yticks(freqs_sub * 2.5)
ax.set_yticklabels([f"f{i}" for i in freqs_sub], fontsize=8)

# Signal OFDM composite
ax2 = axes[1]
ax2.plot(t_ofdm, signal_total, color="#8e44ad", linewidth=1.5)
ax2.fill_between(t_ofdm, signal_total, alpha=0.15, color="#8e44ad")
ax2.set_title("Signal OFDM composite (somme des 8 sous-porteuses)")
ax2.set_xlabel("Temps")
ax2.set_ylabel("Amplitude")

plt.tight_layout()
plt.show()
```

---

## Calculs de débit : exemples pratiques

```{code-cell} python
import math

print("=" * 65)
print("Calculs de débit — Théorèmes de Nyquist et Shannon")
print("=" * 65)

scenarios = [
    {
        "nom": "Câble téléphonique (POTS)",
        "bande_hz": 3400,
        "snr_db": 38,
        "niveaux_nyquist": 8,
        "description": "Ligne téléphonique classique 300–3400 Hz",
    },
    {
        "nom": "ADSL2+",
        "bande_hz": 2_208_000,
        "snr_db": 40,
        "niveaux_nyquist": 512,
        "description": "ADSL2+ montée en débit, bande 138 kHz – 2.2 MHz",
    },
    {
        "nom": "Wi-Fi 802.11n canal 40 MHz",
        "bande_hz": 40_000_000,
        "snr_db": 30,
        "niveaux_nyquist": 64,
        "description": "OFDM, 64-QAM, canal 40 MHz",
    },
    {
        "nom": "Wi-Fi 6 (802.11ax) 160 MHz",
        "bande_hz": 160_000_000,
        "snr_db": 35,
        "niveaux_nyquist": 1024,
        "description": "1024-QAM, canal 160 MHz, 8 flux MIMO",
    },
    {
        "nom": "5G NR mmWave (800 MHz)",
        "bande_hz": 800_000_000,
        "snr_db": 20,
        "niveaux_nyquist": 256,
        "description": "5G millimétrique, canal de 800 MHz",
    },
]

for s in scenarios:
    c_shannon = s["bande_hz"] * math.log2(1 + 10**(s["snr_db"] / 10))
    c_nyquist = 2 * s["bande_hz"] * math.log2(s["niveaux_nyquist"])
    print(f"\n{'─'*65}")
    print(f"  {s['nom']}")
    print(f"  {s['description']}")
    print(f"  Bande passante   : {s['bande_hz']:>15,} Hz")
    print(f"  SNR              : {s['snr_db']:>15} dB")
    print(f"  → Shannon  Cmax  : {c_shannon:>15,.0f} bits/s  ({c_shannon/1e6:.1f} Mbit/s)")
    print(f"  → Nyquist  Dmax  : {c_nyquist:>15,.0f} bits/s  ({c_nyquist/1e6:.1f} Mbit/s)")
    print(f"  Facteur limitant : {'Shannon (bruit)' if c_shannon < c_nyquist else 'Nyquist (niveaux)'}")
```

```{code-cell} python
:tags: [hide-input]
# Graphique comparatif des débits théoriques
fig, ax = plt.subplots(figsize=(11, 6))

noms = [s["nom"] for s in scenarios]
shannon_vals = [s["bande_hz"] * math.log2(1 + 10**(s["snr_db"] / 10)) / 1e6 for s in scenarios]
nyquist_vals = [2 * s["bande_hz"] * math.log2(s["niveaux_nyquist"]) / 1e6 for s in scenarios]

x = np.arange(len(noms))
width = 0.35
bars1 = ax.bar(x - width/2, shannon_vals, width, label="Shannon Cmax", color="#2980b9", alpha=0.85)
bars2 = ax.bar(x + width/2, nyquist_vals, width, label="Nyquist Dmax", color="#e74c3c", alpha=0.85)

for bar, val in zip(bars1, shannon_vals):
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 5,
            f"{val:.0f}", ha="center", va="bottom", fontsize=8, color="#2980b9")
for bar, val in zip(bars2, nyquist_vals):
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 5,
            f"{val:.0f}", ha="center", va="bottom", fontsize=8, color="#e74c3c")

ax.set_yscale("log")
ax.set_ylabel("Débit (Mbit/s) — échelle log")
ax.set_title("Comparaison des débits théoriques (Nyquist vs Shannon)", fontsize=12, fontweight="bold")
ax.set_xticks(x)
ax.set_xticklabels(noms, rotation=20, ha="right", fontsize=9)
ax.legend()
ax.grid(True, axis="y", alpha=0.4)
plt.tight_layout()
plt.show()
```

---

## Résumé

```{admonition} Points clés du chapitre 2
:class: note
- Un **signal analogique** varie continûment ; un **signal numérique** prend des valeurs discrètes.
- Le **théorème de Nyquist** donne le débit max en l'absence de bruit ; **Shannon** tient compte du bruit (SNR).
- La **paire torsadée** (Cat5e–Cat8) couvre jusqu'à 100 m à 1–40 Gbit/s ; la **fibre optique** couvre des dizaines de kilomètres.
- Les **codages en ligne** (NRZ, Manchester, 4B5B) définissent comment les bits sont représentés électriquement.
- La **modulation QAM** encode plusieurs bits par symbole (16-QAM = 4 bits, 256-QAM = 8 bits).
- L'**OFDM** divise le canal en nombreuses sous-porteuses orthogonales pour résister aux interférences sélectives en fréquence.
```

| Technologie | Débit réel | Portée | Fréquence / Média |
|-------------|------------|--------|-------------------|
| Cat5e (1GbE) | 1 Gbit/s | 100 m | Cuivre, paires torsadées |
| Cat6a (10GbE) | 10 Gbit/s | 100 m | Cuivre, paires torsadées |
| Fibre OM4 | 25 Gbit/s | 400 m | Verre, multimode |
| Fibre SMF | 100 Gbit/s+ | > 80 km | Verre, monomode |
| Wi-Fi 6 (2×2) | ~1.2 Gbit/s | ~50 m intérieur | 5/6 GHz, OFDMA |
| 5G mmWave | ~4 Gbit/s | < 500 m | 26–40 GHz, OFDM |
