# -*- coding: utf-8 -*-
"""
Created on Sat Apr 12 19:28:54 2025

@author: AKourgli
"""

# -*- coding: utf-8 -*-
"""
Modifié avec étiquettes binaires sur les signaux modulés
"""
import numpy as np
import matplotlib.pyplot as plt

# Paramètres de base
Nb = 40                # Nombre de bits
Fs = 100               # Fréquence d'échantillonnage
Tb = 1                 # Durée d'un bit
t = np.linspace(0, Tb, int(Fs*Tb), endpoint=False)
fc = 5                 # Fréquence porteuse


# Génération des bits
bits = np.random.randint(0, 2, Nb)

def gray_code(n):
    return n ^ (n >> 1)


def modulate_ook(bits, A1=1.0, A0=0.0):
    """Retourne aussi les bits par symbole"""
    signal = []
    symbols_bits = []
    for bit in bits:
        symbols_bits.append([bit])
        A = A1 if bit == 1 else A0
        s = A * np.cos(2 * np.pi * fc * t)
        signal.extend(s)
    return np.array(signal), symbols_bits

def modulate_ask(n_bits, bits, fc, T, fs, M=4):
    k = int(np.log2(M))
    n_symbols = len(bits) // k
    bits = bits.reshape((n_symbols, k))
    symbols_bits = bits.reshape((-1, k))
    
    # Conversion en indices Gray
    powers = 2 ** np.arange(k-1, -1, -1)
    symbols_int = np.dot(bits, powers)
    gray_indices = [gray_code(s) for s in symbols_int]
    
    # Calcul des amplitudes normalisées [-1, 1]
    amplitudes = (2 * np.array(gray_indices) - (M-1)) / (M-1)

    # Génération du signal modulé
    samples_per_symbol = int(fs * T)
    t_total = np.arange(0, n_symbols * T, 1/fs)
    modulated = np.zeros_like(t_total)
    
    for i, (amp, sym_int) in enumerate(zip(amplitudes, symbols_int)):
        start = i * samples_per_symbol
        end = (i+1) * samples_per_symbol
        t = t_total[start:end] - i*T
        modulated[start:end] = amp * np.cos(2 * np.pi * fc * t)
    return modulated, amplitudes, symbols_bits

def modulate_psk(bits, A=1.0):
    """Retourne les bits par symbole"""
    signal = []
    symbols_bits = []
    for bit in bits:
        symbols_bits.append([bit])
        phase = 0 if bit == 0 else np.pi
        s = A * np.cos(2 * np.pi * fc * t + phase)
        signal.extend(s)
    return np.array(signal), symbols_bits

def modulate_fsk(bits, f0=2, f1=5, A=1.0):
    """Retourne les bits par symbole"""
    signal = []
    symbols_bits = []
    for bit in bits:
        symbols_bits.append([bit])
        f = f0 if bit == 0 else f1
        s = A * np.cos(2 * np.pi * f * t)
        signal.extend(s)
    return np.array(signal), symbols_bits


def modulate_qam(bits, M=16, A=1.0):
    k = int(np.log2(M))
    padding = np.zeros((k - (len(bits) % k)) % k, dtype=int)
    bits_padded = np.concatenate([bits, padding])
    symbols_bits = bits_padded.reshape((-1, k))

    symbols = []
    for i in range(0, len(bits_padded), k):
        symbol_bits = bits_padded[i:i+k]
        split_idx = k//2
        
        # Conversion I/Q en Gray
        I_bits = symbol_bits[:split_idx]
        Q_bits = symbol_bits[split_idx:]
        
        I_binary = int(''.join(map(str, I_bits)), 2)
        Q_binary = int(''.join(map(str, Q_bits)), 2)
        I_gray = gray_code(I_binary)
        Q_gray = gray_code(Q_binary)
        
        # Mapping des niveaux Gray
        max_level = int(np.sqrt(M)) - 1
        levels = np.linspace(-max_level, max_level, int(np.sqrt(M)))
        gray_order = [gray_code(b) for b in range(int(np.sqrt(M)))]
        amp_indices = {gray: idx for idx, gray in enumerate(gray_order)}
        
        I = levels[amp_indices[I_gray]]
        Q = levels[amp_indices[Q_gray]]
        
        symbols.append((I, Q))
    
    # Génération du signal
    signal = []
    for I, Q in symbols:
        t_symbol = np.linspace(0, Tb, int(Fs*Tb), endpoint=False)
        s = I * np.cos(2*np.pi*fc*t_symbol) - Q * np.sin(2*np.pi*fc*t_symbol)
        signal.extend(s)    
    return np.array(signal), symbols, symbols_bits

# ========== GÉNÉRATION DES SIGNAUX ==========
ook_signal, ook_bits = modulate_ook(bits)
ask_signal, ask_amps, ask_bits = modulate_ask(Nb, bits, fc, Tb, Fs)
psk_signal, psk_bits = modulate_psk(bits)
fsk_signal, fsk_bits = modulate_fsk(bits)
qam_signal, qam_syms, qam_bits = modulate_qam(bits)

# ========== AFFICHAGE AVEC ÉTIQUETTES ==========
plt.figure(figsize=(12, 8))


# Configuration commune
samples_per_symbol = len(t)
annotation_style = {'ha':'center', 'va':'bottom', 'fontsize':8, 'color':'red'}

# Signal binaire
plt.subplot(6, 1, 1)
plt.plot(np.repeat(bits, samples_per_symbol), drawstyle='steps-post')
plt.title("Signal binaire original")

# Signal OOK
plt.subplot(6, 1, 2)
plt.plot(ook_signal)
for i, bit in enumerate(ook_bits):
    x = i * samples_per_symbol + samples_per_symbol//2
    y = np.max(ook_signal[i*samples_per_symbol:(i+1)*samples_per_symbol]) + 0.2
    plt.text(x, y, str(bit[0]), **annotation_style)
plt.title("Modulation OOK")

# Signal ASK
plt.subplot(6, 1, 3)
plt.plot(ask_signal)
for i, bits_sym in enumerate(ask_bits):
    x = i * samples_per_symbol + samples_per_symbol//2
    y = ask_amps[i] + 0.3
    plt.text(x, y, ''.join(map(str, bits_sym)), **annotation_style)
plt.title("Modulation ASK")

# Signal PSK
plt.subplot(6, 1, 4)
plt.plot(psk_signal)
for i, bit in enumerate(psk_bits):
    x = i * samples_per_symbol + samples_per_symbol//2
    y = np.max(psk_signal[i*samples_per_symbol:(i+1)*samples_per_symbol]) + 0.2
    plt.text(x, y, str(bit[0]), **annotation_style)
plt.title("Modulation PSK (BPSK)")

# Signal FSK
plt.subplot(6, 1, 5)
plt.plot(fsk_signal)
for i, bit in enumerate(fsk_bits):
    x = i * samples_per_symbol + samples_per_symbol//2
    y = np.max(fsk_signal[i*samples_per_symbol:(i+1)*samples_per_symbol]) + 0.2
    plt.text(x, y, str(bit[0]), **annotation_style)
plt.title("Modulation FSK")

# Signal QAM
plt.subplot(6, 1, 6)
plt.plot(qam_signal)
for i, bits_sym in enumerate(qam_bits):
    x = i * samples_per_symbol + samples_per_symbol//2
    y = np.max(qam_signal[i*samples_per_symbol:(i+1)*samples_per_symbol]) + 0.3
    plt.text(x, y, ''.join(map(str, bits_sym)), **annotation_style)
plt.title("Modulation 16-QAM")

plt.tight_layout()
plt.show()

plt.figure(figsize=(12, 8))


# # Configuration commune
# samples_per_symbol = len(t)
# annotation_style = {'ha':'center', 'va':'bottom', 'fontsize':8, 'color':'red'}

# # Signal binaire
# plt.subplot(6, 1, 1)
# plt.plot(np.repeat(bits, samples_per_symbol), drawstyle='steps-post')
# plt.title("Signal binaire original")

# # Signal OOK
# plt.subplot(6, 1, 2)
# plt.plot(ook_signal)
# for i, bit in enumerate(ook_bits):
#     x = i * samples_per_symbol + samples_per_symbol//2
#     y = np.max(ook_signal[i*samples_per_symbol:(i+1)*samples_per_symbol]) + 0.2
# plt.title("Modulation OOK")

# # Signal ASK
# plt.subplot(6, 1, 3)
# plt.plot(ask_signal)
# for i, bits_sym in enumerate(ask_bits):
#     x = i * samples_per_symbol + samples_per_symbol//2
#     y = ask_amps[i] + 0.3
# plt.title("Modulation ASK")

# # Signal PSK
# plt.subplot(6, 1, 4)
# plt.plot(psk_signal)
# for i, bit in enumerate(psk_bits):
#     x = i * samples_per_symbol + samples_per_symbol//2
#     y = np.max(psk_signal[i*samples_per_symbol:(i+1)*samples_per_symbol]) + 0.2
# plt.title("Modulation PSK (BPSK)")

# # Signal FSK
# plt.subplot(6, 1, 5)
# plt.plot(fsk_signal)
# for i, bit in enumerate(fsk_bits):
#     x = i * samples_per_symbol + samples_per_symbol//2
#     y = np.max(fsk_signal[i*samples_per_symbol:(i+1)*samples_per_symbol]) + 0.2
# plt.title("Modulation FSK")

# # Signal QAM
# plt.subplot(6, 1, 6)
# plt.plot(qam_signal)
# for i, bits_sym in enumerate(qam_bits):
#     x = i * samples_per_symbol + samples_per_symbol//2
#     y = np.max(qam_signal[i*samples_per_symbol:(i+1)*samples_per_symbol]) + 0.3
# plt.title("Modulation 16-QAM")

# plt.tight_layout()
# plt.show()