"""
Simulación de Ecualización de Canal Adaptativa con el algoritmo LMS
====================================================================



Escenario:
  - Se transmiten símbolos BPSK a través de un canal FIR desconocido
    que introduce interferencia intersimbólica (ISI).
  - Se agrega ruido AWGN.
  - Un ecualizador FIR adaptado con LMS intenta invertir el efecto del canal.
  - Fase 1: modo entrenamiento (training mode) - se usa una secuencia conocida.
  - Fase 2: modo decisión dirigida (decision-directed mode) - se usan las
    propias decisiones del receptor para seguir adaptando el filtro.

"""

import numpy as np
import matplotlib.pyplot as plt

# ----------------------------------------------------------------------
# 1. Parámetros de la simulación
# ----------------------------------------------------------------------
np.random.seed(42)              # reproducibilidad

N_TRAIN = 500                   # símbolos usados en fase de entrenamiento
N_DATA = 1500                   # símbolos adicionales en modo decisión dirigida
N_TOTAL = N_TRAIN + N_DATA

SNR_dB = 15                     # relación señal a ruido (dB)
MU = 0.01                       # paso de adaptación (learning rate) del LMS
M = 11                          # número de taps del ecualizador (impar, centrado)
DELAY = M // 2                  # retardo de referencia para alinear d[n] con y[n]

# Respuesta al impulso del canal (multipath simulado)
# Un canal con ISI moderada; ajustar para ver efecto de distorsión mayor/menor
h_channel = np.array([0.9, 0.5, 0.2, 0.4, 0.1])  # h[0], h[1], h[2], h[3], h[4]

# ----------------------------------------------------------------------
# 2. Generación de símbolos BPSK (+1 / -1)
# ----------------------------------------------------------------------
bits = np.random.randint(0, 2, N_TOTAL)
symbols = 2 * bits - 1  # BPSK: 0 -> -1, 1 -> +1

# ----------------------------------------------------------------------
# 3. Paso por el canal (convolución) + ruido AWGN
# ----------------------------------------------------------------------
tx_signal = np.convolve(symbols, h_channel, mode='full')[:N_TOTAL]

signal_power = np.mean(tx_signal ** 2)
noise_power = signal_power / (10 ** (SNR_dB / 10))
noise = np.sqrt(noise_power) * np.random.randn(N_TOTAL)

rx_signal = tx_signal + noise  # señal recibida (distorsionada + ruido)

# ----------------------------------------------------------------------
# 4. Ecualizador adaptativo LMS
# ----------------------------------------------------------------------
w = np.zeros(M)              # coeficientes del ecualizador, inicializados en 0
mse_curve = np.zeros(N_TOTAL)
y_out = np.zeros(N_TOTAL)    # salida del ecualizador (estimación del símbolo)
w_history = np.zeros((N_TOTAL, M))  # evolución de los coeficientes w a lo largo de n

# Buffer de entrada (línea de retardo del filtro FIR)
x_buffer = np.zeros(M)

for n in range(N_TOTAL):
    # Actualizar buffer de entrada (desplazamiento tipo FIFO)
    x_buffer[1:] = x_buffer[:-1]
    x_buffer[0] = rx_signal[n]

    # Salida del ecualizador: y[n] = w^T * x_buffer
    y_out[n] = np.dot(w, x_buffer)

    # Señal deseada d[n]:
    #   - Modo entrenamiento: símbolo transmitido conocido (con el retardo DELAY
    #     para compensar la latencia del canal + ecualizador)
    #   - Modo decisión dirigida: se usa la decisión (signo) de la salida
    ref_idx = n - DELAY
    if ref_idx < 0:
        continue  # aún no hay referencia válida

    if n < N_TRAIN:
        d_n = symbols[ref_idx]           # modo entrenamiento
    else:
        d_n = np.sign(y_out[n])          # modo decisión dirigida
        if d_n == 0:
            d_n = 1

    # Error y actualización de pesos (regla LMS)
    e_n = d_n - y_out[n]
    w += MU * e_n * x_buffer

    mse_curve[n] = e_n ** 2
    w_history[n, :] = w   # se guarda el estado de los coeficientes tras la actualización

# ----------------------------------------------------------------------
# 5. Cálculo de tasa de error de símbolo (SER) antes y después de ecualizar
# ----------------------------------------------------------------------
valid_idx = np.arange(DELAY, N_TOTAL)
decisions_eq = np.sign(y_out[valid_idx])
decisions_eq[decisions_eq == 0] = 1
ref_symbols = symbols[valid_idx - DELAY]

ser_equalized = np.mean(decisions_eq != ref_symbols)

decisions_noeq = np.sign(rx_signal[valid_idx])
decisions_noeq[decisions_noeq == 0] = 1
ser_no_eq = np.mean(decisions_noeq != ref_symbols)

print(f"SER sin ecualizar   : {ser_no_eq:.4f}")
print(f"SER con ecualizador : {ser_equalized:.4f}")
print(f"Coeficientes finales del ecualizador w:\n{np.round(w, 3)}")

# ----------------------------------------------------------------------
# 6. Gráficas
# ----------------------------------------------------------------------
fig, axs = plt.subplots(2, 2, figsize=(12, 9))

# --- (a) Curva de aprendizaje: MSE vs iteración ---
axs[0, 0].plot(mse_curve, linewidth=0.7)
axs[0, 0].axvline(N_TRAIN, color='r', linestyle='--',
                   label='Fin del Entrenamiento')
axs[0, 0].set_title('Curva de aprendizaje del LMS (MSE instantáneo)')
# 
# axs[0, 0].set_xlabel('Iteración (n)')
axs[0, 0].set_ylabel('Error cuadrático')
axs[0, 0].legend()
axs[0, 0].grid(alpha=0.3)
# --- (b) MSE suavizado (media móvil) para ver mejor la convergencia ---
window = 25
mse_smooth = np.convolve(mse_curve, np.ones(window) / window, mode='valid')
axs[0, 1].plot(mse_smooth, linewidth=1.2, color='darkorange')
axs[0, 1].axvline(N_TRAIN, color='r', linestyle='--',
                   label='Fin del Entrenamiento')
axs[0, 1].set_title(f'MSE suavizado (media móvil, ventana={window})')
# axs[0, 1].set_xlabel('Iteración (n)')
axs[0, 1].set_ylabel('MSE promedio')
axs[0, 1].legend()
axs[0, 1].grid(alpha=0.3)

# --- (c) "Constelación" (histograma de amplitudes) antes de ecualizar ---
axs[1, 0].hist(rx_signal[N_TRAIN:], bins=60, color='steelblue', alpha=0.8)
axs[1, 0].set_title('Señal recibida SIN ecualizar (con ISI + ruido)')
axs[1, 0].set_xlabel('Amplitud')
axs[1, 0].set_ylabel('Frecuencia')
axs[1, 0].grid(alpha=0.3)

# --- (d) "Constelación" después de ecualizar ---
axs[1, 1].hist(y_out[N_TRAIN:], bins=60, color='seagreen', alpha=0.8)
axs[1, 1].set_title('Salida del ecualizador (después del LMS)')
axs[1, 1].set_xlabel('Amplitud')
axs[1, 1].set_ylabel('Frecuencia')
axs[1, 1].grid(alpha=0.3)

plt.tight_layout()
plt.savefig('lms_equalizer_resultados.png', dpi=150)
plt.show()

print("\nGráfica guardada como 'lms_equalizer_resultados.png'")

# ----------------------------------------------------------------------
# 7. Evolución de los coeficientes w(n) del ecualizador
# ----------------------------------------------------------------------
fig2, ax2 = plt.subplots(figsize=(11, 6))

for i in range(M):
    ax2.plot(w_history[:, i], linewidth=0.9, label=f'w[{i}]')

ax2.axvline(N_TRAIN, color='r', linestyle='--', label='Fin del entrenamiento')
ax2.set_title('Evolución de los coeficientes w(n) del ecualizador LMS')
ax2.set_xlabel('Iteración (n)')
ax2.set_ylabel('Valor del coeficiente')
ax2.legend(ncol=4, fontsize=8, loc='upper right')
ax2.grid(alpha=0.3)

plt.tight_layout()
plt.savefig('lms_equalizer_coeficientes.png', dpi=150)
plt.show()

print("Gráfica guardada como 'lms_equalizer_coeficientes.png'")
