Diferencia entre revisiones de «Series de Fourier CPP»

De MateWiki
Saltar a: navegación, buscar
(Página creada con «import numpy as np import matplotlib.pyplot as plt from scipy.integrate import trapezoid def base_trigonometrica(n): #Puntos donde se grafica x = np.linspace(-1,1,1000)...»)
 
 
(No se muestran 6 ediciones intermedias del mismo usuario)
Línea 1: Línea 1:
 +
 +
{{ TrabajoED | Series de Fourier. Grupo PCP| [[:Categoría:EDP|EDP]]|[[:Categoría:EDP25/26|2025-26]] | Paula Sánchez,Paula Mellado y Clara Garcia-Hoz}}
 +
 +
[[Archivo:poster.jpg]]
 +
 +
 +
=== Referencias ===
 +
 +
Sandro Salsa — Partial Differential Equations in Action
 +
https://tutorial.math.lamar.edu/Classes/DE/ConvergenceFourierSeries.aspx
 +
 +
https://www.physicsclassroom.com
 +
 +
 +
=== Serie de Fourier ===
 +
 +
 +
[[Categoría:EDP]]
 +
[[Categoría:EDP25/26]]
 +
 +
Abajo se puede ver el código que se ha utilizado para conseguir las gráficas y la serie de Fourier de la función.
 +
<source lang="python" line>
 
import numpy as np
 
import numpy as np
 
import matplotlib.pyplot as plt
 
import matplotlib.pyplot as plt
 
from scipy.integrate import trapezoid
 
from scipy.integrate import trapezoid
  
def base_trigonometrica(n):
+
import numpy as np
#Puntos donde se grafica
+
import matplotlib.pyplot as plt
x = np.linspace(-1,1,1000)
+
plt.figure(figsize = (10,6))
+
  
#Primer término de la serie
+
# ============================================================
plt.plot(x, np.full_like(x, 0.5), label='1/2 (n=0)', linewidth=2, color='black')
+
# 0) SERIE DE FOURIER (texto que se imprime al ejecutar)
 +
# ============================================================
  
#Términos trigonométricos
+
def print_fourier_series_info():
for n in range(1,n):
+
    print("\n" + "="*70)
plt.plot(x, np.cos(n*np.pi*x), color = 'r', linestyle='-')
+
    print("ESTUDIO: Fenómeno de Gibbs en la onda cuadrada 2π-periódica")
plt.plot(x, np.sin(n*np.pi*x), color = 'b', linestyle='-')
+
    print("="*70)
  
plt.title('Primeros términos de la base trigonométrica {1/2, cos(nπx), sin(nπx)}')
+
    print("\nFunción (onda cuadrada 2π-periódica):")
plt.xlabel('x')
+
    print(f(x) = 1  si  0 < x < π")
plt.ylabel('f(x)')
+
    print("  f(x) = -1  si -π < x < 0")
plt.axhline(0, color='black', linewidth=0.5)
+
    print("  y se extiende 2π-periódicamente: f(x+2π)=f(x).")
plt.axvline(0, color='black', linewidth=0.5)
+
plt.grid(True, linestyle=':', alpha=0.6)
+
plt.legend(['1/2','cos','sin'], loc = 'upper right', bbox_to_anchor = (1,1))
+
plt.tight_layout()
+
  
# Mostrar resultado
+
    print("\nSerie de Fourier:")
plt.show()
+
    print("  f(x) ~ (4/π) * Σ_{n=1..∞}  sin((2n-1)x)/(2n-1)")
  
 +
    print("\nSuma parcial (N términos):")
 +
    print("  S_N(x) = (4/π) * Σ_{n=1..N}  sin((2n-1)x)/(2n-1)")
  
def serie_fourier_definitiva(f,L,n):
+
    print("\nHechos clave (Gibbs):")
 +
    print(" - Cerca de las discontinuidades aparecen oscilaciones.")
 +
    print(" - Al aumentar N, las oscilaciones se 'concentran' cerca del salto,")
 +
    print("  pero el sobreimpulso máximo NO desaparece (tiende a una constante).")
 +
    print(" - Lejos del salto, S_N converge mejor (convergencia puntual en continuidad).")
 +
    print("="*70 + "\n")
  
#Puntos donde se grafica y sus valores exactos
 
x = np.linspace(-L/2,L/2,1000)
 
f_ev = f(x)
 
  
def serie_fourier(funcion,x_val,n_terms):
+
# ============================================================
fn = np.zeros_like(x_val)
+
# 1) Definición de la onda cuadrada 2π-periódica
f_eval = funcion(x_val)
+
# ============================================================
  
c0 = trapezoid(2/L * f_eval,x_val)
+
def square_wave_2pi(x):
fn = c0/2
+
    """
for k in range(1,n_terms+1):
+
    Onda cuadrada 2π-periódica con salto en 0 (y ±π).
#Coeficientes
+
    Se define en el intervalo principal (-π, π].
cn = trapezoid(2/L * f_eval * np.cos(2*k*np.pi*x_val/L),x_val)
+
    """
dn = trapezoid(2/L * f_eval * np.sin(2*k*np.pi*x_val/L),x_val)
+
    xr = (x + np.pi) % (2*np.pi) - np.pi  # reduce a (-π, π]
 +
    y = np.where(xr > 0, 1.0, -1.0)
 +
    return y
  
#Sumar términos
 
fn +=  cn* np.cos(2*k*np.pi*x_val/L) + dn * np.sin(2*k*np.pi*x_val/L)
 
  
return fn
+
# ============================================================
 +
# 2) Suma parcial de Fourier
 +
# ============================================================
  
#Gráfica
+
def fourier_partial_sum_square(x, N):
plt.figure(figsize = (10,6))
+
    """
plt.plot(x,f_ev,'k--', label='f(x) original', linewidth=2)
+
    S_N(x) = (4/π) * sum_{n=1..N} sin((2n-1)x)/(2n-1)
 +
    Vectorizado para x array.
 +
    """
 +
    k = 2*np.arange(1, N+1) - 1  # 1,3,5,...,(2N-1)
 +
    return (4/np.pi) * np.sum(np.sin(np.outer(k, x)) / k[:, None], axis=0)
  
for i in n:
 
fn_ev = serie_fourier(f,x,i)
 
plt.plot(x,fn_ev, label=f'n = {i}')
 
  
plt.title('Aproximación de f(x) mediante serie de Fourier')
+
# ============================================================
plt.legend()
+
# 3) DIBUJOS: global + zoom Gibbs
plt.grid(True, alpha=0.3)
+
# ============================================================
plt.show()
+
  
#Cálculo de errores
+
def plot_global_and_zoom():
n_error = []
+
    # Global
for i in range(1,100):
+
    M = 5000
n_error.append(i)
+
    x = np.linspace(-np.pi, np.pi, M, endpoint=True)
 +
    f = square_wave_2pi(x)
  
error_l2 = []
+
    plt.figure()
error_inf = []
+
    plt.plot(x, f, linewidth=2, label="f(x) (onda cuadrada)")
 +
    for N in [1, 3, 10, 50, 100]:
 +
        S = fourier_partial_sum_square(x, N)
 +
        plt.plot(x, S, label=f"S_{N}(x)")
 +
    plt.title("Aproximación global de la onda cuadrada con sumas parciales")
 +
    plt.xlabel("x")
 +
    plt.ylabel("amplitud")
 +
    plt.ylim(-1.6, 1.6)
 +
    plt.grid(True, alpha=0.3)
 +
    plt.legend()
 +
    plt.show()
  
for i in n_error:
+
    # Zoom cerca de x=0
f_n = serie_fourier(f,x,i)
+
    zoom = 0.5
dif = f_ev - f_n
+
    xz = np.linspace(-zoom, zoom, 3000)
 +
    fz = square_wave_2pi(xz)
 +
 
 +
    plt.figure()
 +
    plt.plot(xz, fz, linewidth=2, label="f(x)")
 +
    for N in [5, 10, 20, 50, 100]:
 +
        Sz = fourier_partial_sum_square(xz, N)
 +
        plt.plot(xz, Sz, label=f"S_{N}(x)")
 +
    plt.title("Fenómeno de Gibbs (zoom cerca de la discontinuidad x=0)")
 +
    plt.xlabel("x")
 +
    plt.ylabel("amplitud")
 +
    plt.ylim(-1.6, 1.6)
 +
    plt.grid(True, alpha=0.3)
 +
    plt.legend()
 +
    plt.show()
  
error_l2.append(np.sqrt(trapezoid(dif**2,x)))
 
error_inf.append(round(float(np.max(np.abs(dif))),6))
 
  
#Gráfica
+
# ============================================================
plt.figure(figsize = (10,6))
+
# 4) ANÁLISIS NUMÉRICO del Gibbs: overshoot/undershoot vs N
plt.plot(n_error,error_l2, linewidth=2)
+
# ============================================================
plt.title('Error $L^2$')
+
plt.legend()
+
plt.grid(True, alpha=0.3)
+
plt.show()
+
  
def comparar_fourier_cesaro(func, L, n_terms, nombre_funcion="Función"):
+
def gibbs_numerical_study(Nmax=300, window=0.3):
 
     """
 
     """
     Calcula y grafica la aproximación de Fourier estándar vs Sumas de Cesàro.
+
     Mide numéricamente:
   
+
      - overshoot a la derecha del salto: max(S_N) - 1
    func: Función original a aproximar.
+
      - undershoot a la izquierda: (-1) - min(S_N) con signo ajustado
    L: Longitud del intervalo [-L/2, L/2].
+
     Normaliza por el salto (que vale 2).
    n_terms: Número máximo de coeficientes (n).
+
      
+
 
     """
 
     """
      
+
     jump = 2.0  # salto de -1 a +1
    def get_coefficients(n):
+
        # Integración por método del trapecio como se indica en el documento
+
        x_int = np.linspace(-L/2, L/2, 2000)
+
        y_int = func(x_int)
+
       
+
        # Coeficiente c0
+
        c0 = (2/L) * trapezoid(y_int, x_int)
+
       
+
        an = []
+
        bn = []
+
        for i in range(1, n + 1):
+
            cos_term = np.cos(2 * np.pi * i * x_int / L)
+
            sin_term = np.sin(2 * np.pi * i * x_int / L)
+
            an.append((2/L) * trapezoid(y_int * cos_term, x_int))
+
            bn.append((2/L) * trapezoid(y_int * sin_term, x_int))
+
        return c0, an, bn
+
  
     # Obtener coeficientes
+
     x_right = np.linspace(0, window, 6000, endpoint=True)
    c0, an, bn = get_coefficients(n_terms)
+
     x_left  = np.linspace(-window, 0, 6000, endpoint=True)
      
+
 
    # Construir sumas parciales S_k para Cesàro
+
     overshoot_vals = np.zeros(Nmax)
    x_range = np.linspace(-L/2,L/2,1000)
+
     undershoot_vals = np.zeros(Nmax)
    sumas_parciales = []
+
 
     current_sum = np.full_like(x_range, c0 / 2)
+
     for N in range(1, Nmax+1):
     sumas_parciales.append(current_sum.copy())
+
         Sr = fourier_partial_sum_square(x_right, N)
   
+
        Sl = fourier_partial_sum_square(x_left, N)
     for i in range(n_terms):
+
 
         term = an[i] * np.cos(2 * np.pi * (i+1) * x_range / L) + \
+
        overshoot_vals[N-1] = np.max(Sr) - 1.0
              bn[i] * np.sin(2 * np.pi * (i+1) * x_range / L)
+
         undershoot_vals[N-1] = -1.0 - np.min(Sl)
         current_sum += term
+
 
        sumas_parciales.append(current_sum.copy())
+
     overshoot_rel = overshoot_vals / jump
   
+
     undershoot_rel = undershoot_vals / jump
     # Calcular Suma de Cesàro (Promedio de las sumas parciales)
+
 
     sigma_n = np.mean(sumas_parciales, axis=0)
+
     # Gráfica overshoot/undershoot
   
+
     N_axis = np.arange(1, Nmax+1)
     # Gráfica
+
     plt.figure()
     plt.figure(figsize=(12, 6))
+
     plt.plot(N_axis, overshoot_rel, label="Sobreoscilación / salto")
     plt.plot(x_range, func(x_range), 'k--', label=f"Original: {nombre_funcion}", alpha=0.6)
+
     plt.plot(N_axis, undershoot_rel, label="Suboscilación / salto")
     plt.plot(x_range, current_sum, label=f"Fourier Estándar (n={n_terms})", color='red', alpha=0.5)
+
     plt.title("Medida numérica del Gibbs (normalizado por el salto)")
     plt.plot(x_range, sigma_n, label=f"Suma de Cesàro (n={n_terms})", color='blue', linewidth=2)
+
     plt.xlabel("N (número de términos)")
   
+
     plt.ylabel("amplitud relativa")
     plt.title(f"Aproximación de {nombre_funcion}: Fourier vs Cesàro")
+
     plt.grid(True, alpha=0.3)
     plt.xlabel("x")
+
    plt.legend()
     plt.ylabel("f(x)")
+
     plt.grid(True, linestyle='--', alpha=0.7)
+
 
     plt.show()
 
     plt.show()
  
 +
    # Resumen en consola
 +
    print("\n" + "-"*70)
 +
    print(f"Resumen numérico local (buscado en ventana ±{window} alrededor del salto x=0)")
 +
    print("El salto es 2 (de -1 a +1). Se muestra el sobreimpulso relativo over/jump.")
 +
    print("-"*70)
  
 +
    Ns_show = [5, 10, 20, 50, 100, 200, Nmax]
 +
    print("N\tmax(S_N) der.\tmin(S_N) izq.\tOvershoot\tUndershoot\tOver/jump")
 +
    for N in Ns_show:
 +
        Sr = fourier_partial_sum_square(x_right, N)
 +
        Sl = fourier_partial_sum_square(x_left, N)
 +
        max_right = np.max(Sr)
 +
        min_left  = np.min(Sl)
 +
        over = max_right - 1.0
 +
        under = -1.0 - min_left
 +
        print(f"{N}\t{max_right:+.6f}\t\t{min_left:+.6f}\t\t{over:.6f}\t\t{under:.6f}\t\t{over/jump:.6f}")
  
#Lo que se ejecuta---------------------------------------------------------
+
    # Reporte final (valores límite aproximados numéricamente)
base_trigonometrica(6)
+
    print("\nObservación:")
 +
    print(" - Al crecer N, el sobreimpulso relativo (over/jump) se estabiliza cerca de un valor constante.")
 +
    print(" - Aunque el ancho de la zona oscilatoria se hace más pequeño, la altura máxima no tiende a 0.")
 +
    print("-"*70 + "\n")
  
def f(x):
 
lista = []
 
for i in x:
 
if i < 0:
 
lista.append(0)
 
else:
 
lista.append(1)
 
return np.array(lista)
 
  
def MW(x):
+
# ============================================================
n = 200
+
# 5) CONVERGENCIA LEJOS DE LA DISCONTINUIDAD
a = 1/2
+
# ============================================================
b = 13
+
 
 +
def convergence_away_from_jumps(Nmax=300, exclude=0.15):
 +
    """
 +
    Mide el error supremo ||S_N - f||_∞ evitando:
 +
      - vecindad de x=0
 +
      - vecindad de x=±π
 +
    para mostrar que en zonas continuas el error baja con N.
 +
    """
 +
    M = 6000
 +
    x = np.linspace(-np.pi, np.pi, M, endpoint=True)
 +
    f = square_wave_2pi(x)
 +
 
 +
    mask = (np.abs(x) > exclude) & (np.abs(np.abs(x) - np.pi) > exclude)
 +
 
 +
    errors_sup = np.zeros(Nmax)
 +
    for N in range(1, Nmax+1):
 +
        S = fourier_partial_sum_square(x, N)
 +
        errors_sup[N-1] = np.max(np.abs(S[mask] - f[mask]))
 +
 
 +
    N_axis = np.arange(1, Nmax+1)
 +
    plt.figure()
 +
    plt.plot(N_axis, errors_sup)
 +
    plt.title("Error supremo lejos de discontinuidades (zona continua)")
 +
    plt.xlabel("N (número de términos)")
 +
    plt.ylabel(r"$\|S_N - f\|_\infty$ (fuera de saltos)")
 +
    plt.grid(True, alpha=0.3)
 +
    plt.show()
 +
 
 +
    # Print breve
 +
    print("Error supremo lejos de saltos:")
 +
    print(f" - N=10:  {errors_sup[9]:.6f}")
 +
    print(f" - N=50:  {errors_sup[49]:.6f}")
 +
    print(f" - N=100: {errors_sup[99]:.6f}")
 +
    print(f" - N={Nmax}: {errors_sup[Nmax-1]:.6f}")
 +
    print()
 +
 
 +
 
 +
# ============================================================
 +
# MAIN: que salga TODO al ejecutar
 +
# ============================================================
 +
 
 +
if __name__ == "__main__":
 +
    print_fourier_series_info()
 +
    plot_global_and_zoom()
 +
    gibbs_numerical_study(Nmax=300, window=0.3)
 +
    convergence_away_from_jumps(Nmax=300, exclude=0.15)
 +
 
 +
 
 +
 
 +
 
 +
'''
 +
 
 +
import numpy as np
 +
import matplotlib.pyplot as plt
 +
 
 +
#
 +
# Parámetros (fijamos f0=440)
 +
#
 +
f0 = 440.0                      # Hz
 +
omega0 = 2*np.pi*f0            # rad/s
 +
T = 1.0/f0                      # periodo
 +
 
 +
# Mallado temporal, cambiamos periodos
 +
n_periods = 3
 +
t = np.linspace(0, n_periods*T, 6000)
 +
 
 +
# Coeficientes a_k de la señal (solo senos)
 +
a = {1: 1.0, 2: 0.6, 3: 0.3, 4: 0.1}
 +
 
 +
def F(t):
 +
    return sum(a[k]*np.sin(k*omega0*t) for k in a)
 +
 
 +
def S_N(t, N):
 +
    return sum(a[k]*np.sin(k*omega0*t) for k in a if k <= N)
 +
 
 +
#
 +
# 1) Gráfica: señal original y sumas parciales
 +
#
 +
plt.figure()
 +
plt.plot(t, F(t), label="F(t)")
 +
 
 +
for N in [1, 2, 3, 4]:
 +
    plt.plot(t, S_N(t, N), label=f"S_{N}(t)")
 +
 
 +
plt.xlabel("t (s)")
 +
plt.ylabel("amplitud")
 +
plt.title("F(t) y sumas parciales S_N(t)")
 +
plt.legend()
 +
plt.grid(True)
 +
plt.show()
 +
 
 +
#
 +
# 2) Gráfica: 3er armónico aislado
 +
#
 +
plt.figure()
 +
plt.plot(t, 0.3*np.sin(3*omega0*t), label="F_3(t) = 0.3 sin(3 ω0 t)")
 +
plt.plot(t, F(t), label="F(t)", alpha=0.7)
 +
plt.xlabel("t (s)")
 +
plt.ylabel("amplitud")
 +
plt.title("3er armónico y comparación con la señal completa")
 +
plt.legend()
 +
plt.grid(True)
 +
plt.show()
 +
 
 +
#
 +
# 3) Estudio numérico del error vs N
 +
#  - Error L2 (aprox. por trapecios)
 +
#  - Error sup (norma infinita en la malla)
 +
#
 +
def L2_error(N):
 +
    e = F(t) - S_N(t, N)
 +
    # Integral sobre el intervalo visualizado [0, n_periods*T]
 +
    return np.sqrt(np.trapz(e**2, t))
 +
 
 +
def sup_error(N):
 +
    e = F(t) - S_N(t, N)
 +
    return np.max(np.abs(e))
 +
 
 +
Ns = np.arange(1, 9)
 +
err_L2 = np.array([L2_error(N) for N in Ns])
 +
err_sup = np.array([sup_error(N) for N in Ns])
 +
 
 +
plt.figure()
 +
plt.plot(Ns, err_L2, marker="o", label="Error L2 (numérico)")
 +
plt.plot(Ns, err_sup, marker="o", label="Error sup (malla)")
 +
plt.xlabel("N (número de términos)")
 +
plt.ylabel("error")
 +
plt.title("Convergencia numérica: error vs N")
 +
plt.legend()
 +
plt.grid(True)
 +
plt.show()
 +
'''
 +
 
 +
'''
 +
import numpy as np
 +
import matplotlib.pyplot as plt
 +
 
 +
# Dominio
 +
x = np.linspace(-np.pi, np.pi, 4000)
 +
 
 +
# Onda cuadrada 2π-periódica: -1 en (-π,0), +1 en (0,π)
 +
f = np.where(x < 0, -1.0, 1.0)
 +
f[np.isclose(x, 0.0)] = 0.0  # en el salto el valor da igual para el dibujo
 +
 
 +
def fourier_square_partial_sum(x, N):
 +
    """
 +
    Suma parcial S_N de la serie de Fourier de la onda cuadrada:
 +
    f(x) ~ (4/π) * sum_{k=1..∞} sin((2k-1)x)/(2k-1)
 +
    """
 +
    k = np.arange(1, N + 1)
 +
    odd = 2 * k - 1                      # 1,3,5,7,...
 +
    terms = np.sin(np.outer(odd, x)) / odd[:, None]
 +
    return (4/np.pi) * terms.sum(axis=0)
  
suma = 0
+
N_list = [3, 10, 50]
for i in range(n):
+
plt.figure(figsize=(10, 4.8))
suma += a**i * np.cos(b**i * np.pi*x)
+
plt.plot(x, f, linewidth=2, label="Onda cuadrada f(x)")
  
return suma
+
for N in N_list:
 +
    plt.plot(x, fourier_square_partial_sum(x, N), linewidth=1.5, label=f"Suma parcial N={N}")
  
def g(x):
+
plt.xlim(-np.pi, np.pi)
return x**2
+
plt.ylim(-1.6, 1.6)
 +
plt.xlabel("x")
 +
plt.ylabel("amplitud")
 +
plt.title("Fenómeno de Gibbs: aproximación de una onda cuadrada con series de Fourier")
 +
plt.grid(True, alpha=0.25)
 +
plt.legend()
 +
plt.tight_layout()
 +
plt.show()
 +
'''
 +
</source>
  
serie_fourier_definitiva(f,10,[1,5,10,100])
+
[[Categoría:EDP]]
comparar_fourier_cesaro(f, 10, 50, "Función Discontinua (Escalón)")
+
[[Categoría:EDP25/26]]
comparar_fourier_cesaro(MW, 10, 100, "Monstruo de Weierstrass")
+

Revisión actual del 10:05 19 feb 2026

Trabajo realizado por estudiantes
Título Series de Fourier. Grupo PCP
Asignatura EDP
Curso 2025-26
Autores Paula Sánchez,Paula Mellado y Clara Garcia-Hoz
Este artículo ha sido escrito por estudiantes como parte de su evaluación en la asignatura


Poster.jpg


1 Referencias

Sandro Salsa — Partial Differential Equations in Action https://tutorial.math.lamar.edu/Classes/DE/ConvergenceFourierSeries.aspx

https://www.physicsclassroom.com


2 Serie de Fourier

Abajo se puede ver el código que se ha utilizado para conseguir las gráficas y la serie de Fourier de la función.

import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import trapezoid

import numpy as np
import matplotlib.pyplot as plt

# ============================================================
# 0) SERIE DE FOURIER (texto que se imprime al ejecutar)
# ============================================================

def print_fourier_series_info():
    print("\n" + "="*70)
    print("ESTUDIO: Fenómeno de Gibbs en la onda cuadrada 2π-periódica")
    print("="*70)

    print("\nFunción (onda cuadrada 2π-periódica):")
    print("   f(x) =  1   si  0 < x < π")
    print("   f(x) = -1   si -π < x < 0")
    print("   y se extiende 2π-periódicamente: f(x+2π)=f(x).")

    print("\nSerie de Fourier:")
    print("   f(x) ~ (4/π) * Σ_{n=1..∞}  sin((2n-1)x)/(2n-1)")

    print("\nSuma parcial (N términos):")
    print("   S_N(x) = (4/π) * Σ_{n=1..N}  sin((2n-1)x)/(2n-1)")

    print("\nHechos clave (Gibbs):")
    print(" - Cerca de las discontinuidades aparecen oscilaciones.")
    print(" - Al aumentar N, las oscilaciones se 'concentran' cerca del salto,")
    print("   pero el sobreimpulso máximo NO desaparece (tiende a una constante).")
    print(" - Lejos del salto, S_N converge mejor (convergencia puntual en continuidad).")
    print("="*70 + "\n")


# ============================================================
# 1) Definición de la onda cuadrada 2π-periódica
# ============================================================

def square_wave_2pi(x):
    """
    Onda cuadrada 2π-periódica con salto en 0 (y ±π).
    Se define en el intervalo principal (-π, π].
    """
    xr = (x + np.pi) % (2*np.pi) - np.pi  # reduce a (-π, π]
    y = np.where(xr > 0, 1.0, -1.0)
    return y


# ============================================================
# 2) Suma parcial de Fourier
# ============================================================

def fourier_partial_sum_square(x, N):
    """
    S_N(x) = (4/π) * sum_{n=1..N} sin((2n-1)x)/(2n-1)
    Vectorizado para x array.
    """
    k = 2*np.arange(1, N+1) - 1  # 1,3,5,...,(2N-1)
    return (4/np.pi) * np.sum(np.sin(np.outer(k, x)) / k[:, None], axis=0)


# ============================================================
# 3) DIBUJOS: global + zoom Gibbs
# ============================================================

def plot_global_and_zoom():
    # Global
    M = 5000
    x = np.linspace(-np.pi, np.pi, M, endpoint=True)
    f = square_wave_2pi(x)

    plt.figure()
    plt.plot(x, f, linewidth=2, label="f(x) (onda cuadrada)")
    for N in [1, 3, 10, 50, 100]:
        S = fourier_partial_sum_square(x, N)
        plt.plot(x, S, label=f"S_{N}(x)")
    plt.title("Aproximación global de la onda cuadrada con sumas parciales")
    plt.xlabel("x")
    plt.ylabel("amplitud")
    plt.ylim(-1.6, 1.6)
    plt.grid(True, alpha=0.3)
    plt.legend()
    plt.show()

    # Zoom cerca de x=0
    zoom = 0.5
    xz = np.linspace(-zoom, zoom, 3000)
    fz = square_wave_2pi(xz)

    plt.figure()
    plt.plot(xz, fz, linewidth=2, label="f(x)")
    for N in [5, 10, 20, 50, 100]:
        Sz = fourier_partial_sum_square(xz, N)
        plt.plot(xz, Sz, label=f"S_{N}(x)")
    plt.title("Fenómeno de Gibbs (zoom cerca de la discontinuidad x=0)")
    plt.xlabel("x")
    plt.ylabel("amplitud")
    plt.ylim(-1.6, 1.6)
    plt.grid(True, alpha=0.3)
    plt.legend()
    plt.show()


# ============================================================
# 4) ANÁLISIS NUMÉRICO del Gibbs: overshoot/undershoot vs N
# ============================================================

def gibbs_numerical_study(Nmax=300, window=0.3):
    """
    Mide numéricamente:
      - overshoot a la derecha del salto: max(S_N) - 1
      - undershoot a la izquierda: (-1) - min(S_N) con signo ajustado
    Normaliza por el salto (que vale 2).
    """
    jump = 2.0  # salto de -1 a +1

    x_right = np.linspace(0, window, 6000, endpoint=True)
    x_left  = np.linspace(-window, 0, 6000, endpoint=True)

    overshoot_vals = np.zeros(Nmax)
    undershoot_vals = np.zeros(Nmax)

    for N in range(1, Nmax+1):
        Sr = fourier_partial_sum_square(x_right, N)
        Sl = fourier_partial_sum_square(x_left, N)

        overshoot_vals[N-1] = np.max(Sr) - 1.0
        undershoot_vals[N-1] = -1.0 - np.min(Sl)

    overshoot_rel = overshoot_vals / jump
    undershoot_rel = undershoot_vals / jump

    # Gráfica overshoot/undershoot
    N_axis = np.arange(1, Nmax+1)
    plt.figure()
    plt.plot(N_axis, overshoot_rel, label="Sobreoscilación / salto")
    plt.plot(N_axis, undershoot_rel, label="Suboscilación / salto")
    plt.title("Medida numérica del Gibbs (normalizado por el salto)")
    plt.xlabel("N (número de términos)")
    plt.ylabel("amplitud relativa")
    plt.grid(True, alpha=0.3)
    plt.legend()
    plt.show()

    # Resumen en consola
    print("\n" + "-"*70)
    print(f"Resumen numérico local (buscado en ventana ±{window} alrededor del salto x=0)")
    print("El salto es 2 (de -1 a +1). Se muestra el sobreimpulso relativo over/jump.")
    print("-"*70)

    Ns_show = [5, 10, 20, 50, 100, 200, Nmax]
    print("N\tmax(S_N) der.\tmin(S_N) izq.\tOvershoot\tUndershoot\tOver/jump")
    for N in Ns_show:
        Sr = fourier_partial_sum_square(x_right, N)
        Sl = fourier_partial_sum_square(x_left, N)
        max_right = np.max(Sr)
        min_left  = np.min(Sl)
        over = max_right - 1.0
        under = -1.0 - min_left
        print(f"{N}\t{max_right:+.6f}\t\t{min_left:+.6f}\t\t{over:.6f}\t\t{under:.6f}\t\t{over/jump:.6f}")

    # Reporte final (valores límite aproximados numéricamente)
    print("\nObservación:")
    print(" - Al crecer N, el sobreimpulso relativo (over/jump) se estabiliza cerca de un valor constante.")
    print(" - Aunque el ancho de la zona oscilatoria se hace más pequeño, la altura máxima no tiende a 0.")
    print("-"*70 + "\n")


# ============================================================
# 5) CONVERGENCIA LEJOS DE LA DISCONTINUIDAD
# ============================================================

def convergence_away_from_jumps(Nmax=300, exclude=0.15):
    """
    Mide el error supremo ||S_N - f||_∞ evitando:
      - vecindad de x=0
      - vecindad de x=±π
    para mostrar que en zonas continuas el error baja con N.
    """
    M = 6000
    x = np.linspace(-np.pi, np.pi, M, endpoint=True)
    f = square_wave_2pi(x)

    mask = (np.abs(x) > exclude) & (np.abs(np.abs(x) - np.pi) > exclude)

    errors_sup = np.zeros(Nmax)
    for N in range(1, Nmax+1):
        S = fourier_partial_sum_square(x, N)
        errors_sup[N-1] = np.max(np.abs(S[mask] - f[mask]))

    N_axis = np.arange(1, Nmax+1)
    plt.figure()
    plt.plot(N_axis, errors_sup)
    plt.title("Error supremo lejos de discontinuidades (zona continua)")
    plt.xlabel("N (número de términos)")
    plt.ylabel(r"$\|S_N - f\|_\infty$ (fuera de saltos)")
    plt.grid(True, alpha=0.3)
    plt.show()

    # Print breve
    print("Error supremo lejos de saltos:")
    print(f" - N=10:  {errors_sup[9]:.6f}")
    print(f" - N=50:  {errors_sup[49]:.6f}")
    print(f" - N=100: {errors_sup[99]:.6f}")
    print(f" - N={Nmax}: {errors_sup[Nmax-1]:.6f}")
    print()


# ============================================================
# MAIN: que salga TODO al ejecutar
# ============================================================

if __name__ == "__main__":
    print_fourier_series_info()
    plot_global_and_zoom()
    gibbs_numerical_study(Nmax=300, window=0.3)
    convergence_away_from_jumps(Nmax=300, exclude=0.15)




'''

import numpy as np
import matplotlib.pyplot as plt

#
# Parámetros (fijamos f0=440)
#
f0 = 440.0                      # Hz
omega0 = 2*np.pi*f0             # rad/s
T = 1.0/f0                      # periodo

# Mallado temporal, cambiamos periodos
n_periods = 3
t = np.linspace(0, n_periods*T, 6000)

# Coeficientes a_k de la señal (solo senos)
a = {1: 1.0, 2: 0.6, 3: 0.3, 4: 0.1}

def F(t):
    return sum(a[k]*np.sin(k*omega0*t) for k in a)

def S_N(t, N):
    return sum(a[k]*np.sin(k*omega0*t) for k in a if k <= N)

#
# 1) Gráfica: señal original y sumas parciales
# 
plt.figure()
plt.plot(t, F(t), label="F(t)")

for N in [1, 2, 3, 4]:
    plt.plot(t, S_N(t, N), label=f"S_{N}(t)")

plt.xlabel("t (s)")
plt.ylabel("amplitud")
plt.title("F(t) y sumas parciales S_N(t)")
plt.legend()
plt.grid(True)
plt.show()

# 
# 2) Gráfica: 3er armónico aislado
#
plt.figure()
plt.plot(t, 0.3*np.sin(3*omega0*t), label="F_3(t) = 0.3 sin(3 ω0 t)")
plt.plot(t, F(t), label="F(t)", alpha=0.7)
plt.xlabel("t (s)")
plt.ylabel("amplitud")
plt.title("3er armónico y comparación con la señal completa")
plt.legend()
plt.grid(True)
plt.show()

#
# 3) Estudio numérico del error vs N
#   - Error L2 (aprox. por trapecios)
#   - Error sup (norma infinita en la malla)
# 
def L2_error(N):
    e = F(t) - S_N(t, N)
    # Integral sobre el intervalo visualizado [0, n_periods*T]
    return np.sqrt(np.trapz(e**2, t))

def sup_error(N):
    e = F(t) - S_N(t, N)
    return np.max(np.abs(e))

Ns = np.arange(1, 9)
err_L2 = np.array([L2_error(N) for N in Ns])
err_sup = np.array([sup_error(N) for N in Ns])

plt.figure()
plt.plot(Ns, err_L2, marker="o", label="Error L2 (numérico)")
plt.plot(Ns, err_sup, marker="o", label="Error sup (malla)")
plt.xlabel("N (número de términos)")
plt.ylabel("error")
plt.title("Convergencia numérica: error vs N")
plt.legend()
plt.grid(True)
plt.show()
'''

'''
import numpy as np
import matplotlib.pyplot as plt

# Dominio
x = np.linspace(-np.pi, np.pi, 4000)

# Onda cuadrada 2π-periódica: -1 en (-π,0), +1 en (0,π)
f = np.where(x < 0, -1.0, 1.0)
f[np.isclose(x, 0.0)] = 0.0  # en el salto el valor da igual para el dibujo

def fourier_square_partial_sum(x, N):
    """
    Suma parcial S_N de la serie de Fourier de la onda cuadrada:
    f(x) ~ (4/π) * sum_{k=1..∞} sin((2k-1)x)/(2k-1)
    """
    k = np.arange(1, N + 1)
    odd = 2 * k - 1                       # 1,3,5,7,...
    terms = np.sin(np.outer(odd, x)) / odd[:, None]
    return (4/np.pi) * terms.sum(axis=0)

N_list = [3, 10, 50]
plt.figure(figsize=(10, 4.8))
plt.plot(x, f, linewidth=2, label="Onda cuadrada f(x)")

for N in N_list:
    plt.plot(x, fourier_square_partial_sum(x, N), linewidth=1.5, label=f"Suma parcial N={N}")

plt.xlim(-np.pi, np.pi)
plt.ylim(-1.6, 1.6)
plt.xlabel("x")
plt.ylabel("amplitud")
plt.title("Fenómeno de Gibbs: aproximación de una onda cuadrada con series de Fourier")
plt.grid(True, alpha=0.25)
plt.legend()
plt.tight_layout()
plt.show()
'''