%load_ext autoreload
%autoreload 2
import sys
import os
sys.path.append(os.path.abspath(".."))
from utils.plot_helpers import style_math_axesEjemplos de determinación de la SF¶
En función de cómo de complicada sea la señal x~(t), hay dos formas de determinar los coeficientes de la serie de Fourier:
Señales que se pueden expresar de forma inmediata como suma de señales (co)senoidales y exponenciales complejas ⇒ Identificar con la ecuación de síntesis:
Señales que no se pueden expresar de forma inmediata como suma de señales (co)senoidales y exponenciales complejas ⇒ Aplicar la ecuación de análisis:
Además, siempre se pueden usar las tablas de pares transformados + propiedades (ver Propiedades y pares de la SF en tiempo continuo).
Solution to Exercise 1
La frecuencia fundamental y periodo fundamental de la señal x~(t) será:
Convergencia:
Para obtener los coeficientes, identificamos con la ecuación de síntesis:
Como x~(t) es real, se debe cumplir ck∗=c−k:
En la siguiente gráfica interactiva se muestra la señal x~(t)=cos(ω0t) y los coeficientes de su serie de Fourier. Se pueden variar los parámetros T0 (periodo fundamental) y ω0 (frecuencia fundamental), relacionados entre sí.
Observa:
Al variar el periodo fundamental, varía la frecuecia fundamental en relación inversa.
Los coeficientes de la serie de Fourier no varían.
import numpy as np
from bokeh.plotting import figure, show
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, CustomJS, Slider, LabelSet, Div, Label
from bokeh.io import output_notebook
# Importamos las funciones auxiliares
from utils.plot_helpers import style_math_axes, add_math_ticks
output_notebook()
# ==========================================
# 1. PARÁMETROS INICIALES
# ==========================================
# x(t) = cos(w0 * t)
VAL_W0 = np.pi # Frecuencia angular inicial (3.14 rad/s)
VAL_T0 = 2 * np.pi / VAL_W0 # Periodo inicial (2 s)
N_HARMONICS = 5
COLOR_t = 'blue'
COLOR_k = 'blue'
t_min, t_max = -4.0, 4.0
num_points = 2000
t_vals = np.linspace(t_min, t_max, num_points)
# ==========================================
# 2. GENERACIÓN DE DATOS
# ==========================================
# A. Datos Tiempo
def generate_cosine(t_arr, w0):
return np.cos(w0 * t_arr)
y_vals = generate_cosine(t_vals, VAL_W0)
source_time = ColumnDataSource(data=dict(t=t_vals, y=y_vals))
# Ticks Dinámicos Eje X (Tiempo) -> Marcan el periodo
ticks_x = [-VAL_T0, -VAL_T0/2, 0, VAL_T0/2, VAL_T0]
ticks_labels = ["-T0", "-T0/2", "0", "T0/2", "T0"]
source_ticks_x = ColumnDataSource(data=dict(x=ticks_x, y=[0]*5, text=ticks_labels))
# B. Datos Frecuencia
# Para cos(w0 t), ck = 0.5 en k=1, -1.
k_vals = np.arange(-N_HARMONICS, N_HARMONICS + 1)
def calc_ck_cosine():
ck = np.zeros_like(k_vals, dtype=float)
idx_p1 = np.where(k_vals == 1)[0]
idx_m1 = np.where(k_vals == -1)[0]
if len(idx_p1) > 0: ck[idx_p1] = 0.5
if len(idx_m1) > 0: ck[idx_m1] = 0.5
return ck
ck_vals = calc_ck_cosine()
source_freq = ColumnDataSource(data=dict(k=k_vals, ck=ck_vals, zeros=np.zeros_like(k_vals)))
source_ytick_freq = ColumnDataSource(data=dict(x=[0], y=[0.5]))
# ==========================================
# 3. GRÁFICOS
# ==========================================
# --- TIEMPO ---
p_time = figure(width=600, height=300)
style_math_axes(p_time, x_range=(t_min, t_max), y_range=(-1.5, 1.5), xlabel="t", ylabel=r"$$\tilde{x}(t)=\cos(\omega_0 t)$$")
# Ticks verticales en -1 y 1
add_math_ticks(p_time, yticks=[-1, 1], ytick_labels=["-1", "1"], tick_len=5)
p_time.line('t', 'y', source=source_time, color=COLOR_t, line_width=2.5)
# Ticks Dinámicos X (Labels T0)
p_time.scatter(x='x', y='y', source=source_ticks_x, marker="dash", angle=1.57, size=10, color="black")
labels_dyn = LabelSet(x='x', y='y', text='text', source=source_ticks_x,
text_align='center', text_baseline='middle',
y_offset=-20, text_font_style='italic', text_color="black", text_font_size="10pt")
p_time.add_layout(labels_dyn)
# --- FRECUENCIA ---
p_freq = figure(width=600, height=300)
style_math_axes(p_freq, x_range=(-4, 4), y_range=(-0.1, 0.8), xlabel="k", ylabel=r"$$c_k$$")
# Ticks Fijos X
p_freq.scatter(x=[-3, -1, 1, 3], y=0, marker="dash", angle=1.57, size=8, color="black")
for x, txt in zip([-3, -1, 1, 3], ["-3", "-1", "1", "3"]):
p_freq.add_layout(Label(x=x, y=0, text=txt, y_offset=-20, text_align='center', text_font_size="9pt"))
# Tick Fijo Y (0.5)
p_freq.scatter(x='x', y='y', source=source_ytick_freq, marker="dash", angle=0, size=10, color="black")
p_freq.add_layout(Label(x=0, y=0.5, text="1/2", text_align='right', text_baseline='middle', x_offset=-10, text_color="black"))
# Stems
p_freq.segment(x0='k', y0='zeros', x1='k', y1='ck', source=source_freq, color=COLOR_k, line_width=3)
p_freq.scatter('k', 'ck', source=source_freq, color=COLOR_k, size=8, marker="circle")
# ==========================================
# 4. INTERACTIVIDAD (SLIDERS LIGADOS)
# ==========================================
# Definimos los sliders
# Nota: Los rangos deben ser matemáticamente consistentes (inversos)
# Si T0 va de 0.5 a 8.0, w0 debe ir de (2pi/8) a (2pi/0.5)
s_w0 = Slider(start=2*np.pi/8, end=2*np.pi/0.5, value=VAL_W0, step=0.01, title=r"Frecuencia angular (ω0)")
s_T0 = Slider(start=0.5, end=8.0, value=VAL_T0, step=0.01, title=r"Periodo fundamental (T0)")
# Creamos un ÚNICO callback para ambos
callback = CustomJS(
args=dict(source_t=source_time, source_ticks_x=source_ticks_x,
s_w0=s_w0, s_T0=s_T0),
code="""
const PI = Math.PI;
// Inicializamos las variables
let w0 = s_w0.value;
let T0 = s_T0.value;
// LÓGICA DE SINCRONIZACIÓN
// 'cb_obj' es el objeto que disparó el evento (el slider que movió el usuario)
if (cb_obj === s_w0) {
// Si el usuario movió w0 -> calculamos T0 y actualizamos su slider
T0 = 2 * PI / w0;
s_T0.value = T0;
}
else if (cb_obj === s_T0) {
// Si el usuario movió T0 -> calculamos w0 y actualizamos su slider
w0 = 2 * PI / T0;
s_w0.value = w0;
}
// --- A PARTIR DE AQUÍ USAMOS LOS VALORES CALCULADOS PARA DIBUJAR ---
// 1. TIEMPO
const t = source_t.data['t'];
const y = source_t.data['y'];
for (let i = 0; i < t.length; i++) {
y[i] = Math.cos(w0 * t[i]);
}
source_t.change.emit();
// 2. TICKS X (Marcas de Periodo)
const tx = source_ticks_x.data['x'];
tx[0] = -T0;
tx[1] = -T0/2;
tx[2] = 0;
tx[3] = T0/2;
tx[4] = T0;
source_ticks_x.change.emit();
""")
# Asignamos EL MISMO callback a ambos sliders
s_w0.js_on_change('value', callback)
s_T0.js_on_change('value', callback)
# ==========================================
# 5. LAYOUT
# ==========================================
caption_text = """
<div style="font-family: sans-serif; margin-top: 10px; font-size: 14px; opacity: 0.8;">
<b>Señal coseno:</b>
<ul>
<li>Representación temporal.</li>
<li>Coeficientes de su serie de Fourier.</li>
</ul>
</div>
"""
caption = Div(text=caption_text)
layout = column(s_w0, s_T0, p_time, p_freq, caption, sizing_mode="scale_width")
show(layout)Solution to Exercise 2
La frecuencia fundamental de la señal x~(t) será:
Convergencia:
Para obtener los coeficientes, como no es evidente cómo identificar con la ecuación de síntesis, aplicamos la ecuación de análisis:
Para k=0 tenemos una indeterminación (0/0). Cuando ocurre esto, lo más sencillo es calcular el coeficiente c0 por separado, aplicando la ecuación de análisis particularizada para k=0:
Se puede ver que el coeficiente c0 siempre corresponde al valor medio de la señal.
Por tanto,
También podemos ponerlo en forma de sinc:
Como x~(t) es real, se debe cumplir ck∗=c−k:
En la siguiente gráfica interactiva se muestra la señal x~(t) y los coeficientes de su serie de Fourier. Se pueden variar los parámetros T0 (periodo) y el ciclo de trabajo 2T1/T0 (relación entre ancho del pulso y periodo).
Observa:
Variar el periodo no cambia los coeficientes de su serie de Fourier (sí cambia la suma al cambiar ω0).
Variar el ciclo de trabajo sí cambia los coeficientes de la serie de Fourier.
Poner el ciclo de trabajo a 0.5 elimina todos los armónicos pares.
import numpy as np
from bokeh.plotting import figure, show
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, CustomJS, Slider, LabelSet, Div, Label
from bokeh.io import output_notebook
# Importamos style_math_axes
from utils.plot_helpers import style_math_axes
output_notebook()
# ==========================================
# 1. PARÁMETROS INICIALES
# ==========================================
VAL_T0 = 2.0
VAL_DUTY = 0.5 # Valor inicial de (2T1 / T0). Equivale a un Duty Cycle del 50%
# T1 se despeja: 2T1/T0 = Duty => T1 = (Duty * T0) / 2
VAL_T1 = (VAL_DUTY * VAL_T0) / 2
N_HARMONICS = 20
COLOR_t = 'blue'
COLOR_k = 'blue'
t_min, t_max = -4.0, 4.0
num_points = 2000
t_vals = np.linspace(t_min, t_max, num_points)
# ==========================================
# 2. GENERACIÓN DE DATOS
# ==========================================
# A. Datos Tiempo
def generate_square_wave(t_arr, T0, T1):
y = np.zeros_like(t_arr)
t_mod = (t_arr + T0/2) % T0 - T0/2
mask = np.abs(t_mod) <= (T1 - 1e-9)
y[mask] = 1.0
return y
y_vals = generate_square_wave(t_vals, VAL_T0, VAL_T1)
source_time = ColumnDataSource(data=dict(t=t_vals, y=y_vals))
# Ticks Dinámicos Eje X (Tiempo)
ticks_x = [-VAL_T0, -VAL_T1, 0, VAL_T1, VAL_T0]
ticks_labels = ["-T0", "-T1", "0", "T1", "T0"]
source_ticks_x = ColumnDataSource(data=dict(x=ticks_x, y=[0]*5, text=ticks_labels))
# B. Datos Frecuencia y Tick Y
# El valor máximo de la sinc (c0) es exactamente el Duty Cycle (2T1/T0)
val_c0_init = VAL_DUTY
source_ytick = ColumnDataSource(data=dict(x=[0], y=[val_c0_init]))
k_vals = np.arange(-N_HARMONICS, N_HARMONICS + 1)
k_cont = np.linspace(-N_HARMONICS, N_HARMONICS, 400)
def calc_ck(duty):
# La amplitud ahora ES directamente el parámetro duty
def get_sinc(k_in):
if abs(k_in) < 1e-9: return duty
arg = k_in * duty
return duty * np.sin(np.pi * arg) / (np.pi * arg)
c_vals = [get_sinc(k) for k in k_vals]
e_vals = [get_sinc(k) for k in k_cont]
return c_vals, e_vals
ck_vals, env_vals = calc_ck(VAL_DUTY)
source_freq = ColumnDataSource(data=dict(k=k_vals, ck=ck_vals, zeros=np.zeros_like(k_vals)))
source_envelope = ColumnDataSource(data=dict(k=k_cont, val=env_vals))
# ==========================================
# 3. GRÁFICOS
# ==========================================
# --- TIEMPO ---
p_time = figure(width=600, height=300)
style_math_axes(p_time, x_range=(t_min, t_max), y_range=(-0.2, 1.2), xlabel="t", ylabel=r"$$\tilde{x}(t)$$")
p_time.line('t', 'y', source=source_time, color=COLOR_t, line_width=2.5)
# Ticks X Dinámicos
p_time.scatter(x='x', y='y', source=source_ticks_x, marker="dash", angle=1.57, size=10, color="black")
labels_dyn = LabelSet(x='x', y='y', text='text', source=source_ticks_x,
text_align='center', text_baseline='middle',
y_offset=-20, text_font_style='italic', text_color="black", text_font_size="10pt")
p_time.add_layout(labels_dyn)
# --- FRECUENCIA ---
p_freq = figure(width=600, height=300)
style_math_axes(p_freq, x_range=(-N_HARMONICS-1, N_HARMONICS+1), y_range=(-0.1, 1.0), xlabel="k", ylabel=r"$$c_k$$")
# Ticks Fijos X (Referencia)
p_freq.scatter(x=[-10, -5, 5, 10], y=0, marker="dash", angle=1.57, size=8, color="black")
for x, txt in zip([-10, 5, 5, 10], ["-10", "-5", "5", "10"]):
p_freq.add_layout(Label(x=x, y=0, text=txt, y_offset=-20, text_align='center', text_font_size="9pt"))
# >>> TICK DINÁMICO Y (c0) <<<
# 1. La rayita horizontal en el eje Y (x=0)
p_freq.scatter(x='x', y='y', source=source_ytick, marker="dash", angle=0, size=10, color="black")
# 2. La Etiqueta LaTeX
# Usamos Label directamente para soportar LaTeX y la actualizamos en JS
lbl_c0 = Label(x=0, y=val_c0_init, text=r"$$\frac{2T_1}{T_0}$$",
text_align='right', text_baseline='middle',
x_offset=-12, text_color="black", text_font_size="9pt")
p_freq.add_layout(lbl_c0)
# Gráficas
p_freq.line('k', 'val', source=source_envelope, color="gray", line_dash="dashed", alpha=0.6)
p_freq.segment(x0='k', y0='zeros', x1='k', y1='ck', source=source_freq, color=COLOR_k, line_width=2)
p_freq.scatter('k', 'ck', source=source_freq, color=COLOR_k, size=6, marker="circle")
# ==========================================
# 4. INTERACTIVIDAD
# ==========================================
s_T0 = Slider(start=1.0, end=5.0, value=VAL_T0, step=0.1, title=r"Periodo (T0)")
# >>> CAMBIO: El slider ahora controla el Duty Cycle completo (2T1/T0) <<<
s_duty = Slider(start=0.1, end=1.0, value=VAL_DUTY, step=0.01, title=r"Ciclo de Trabajo (2 T1 / T0)")
callback = CustomJS(
args=dict(source_t=source_time, source_f=source_freq, source_env=source_envelope,
source_ticks_x=source_ticks_x, source_ytick=source_ytick,
lbl_c0=lbl_c0,
s_T0=s_T0, s_duty=s_duty),
code="""
const T0 = s_T0.value;
const duty = s_duty.value; // Esto es (2 * T1 / T0)
// Despejamos T1 para dibujar en el tiempo
// 2*T1/T0 = duty => 2*T1 = duty*T0 => T1 = (duty*T0)/2
const T1 = (duty * T0) / 2;
// --- 1. TIEMPO ---
const t = source_t.data['t'];
const y = source_t.data['y'];
for (let i = 0; i < t.length; i++) {
let val = (t[i] + T0/2) % T0;
if (val < 0) val += T0;
val -= T0/2;
y[i] = (Math.abs(val) <= T1) ? 1.0 : 0.0;
}
source_t.change.emit();
// --- 2. TICKS X ---
const tx = source_ticks_x.data['x'];
tx[0] = -T0; tx[1] = -T1; tx[2] = 0; tx[3] = T1; tx[4] = T0;
source_ticks_x.change.emit();
// --- 3. FRECUENCIA ---
const k = source_f.data['k'];
const ck = source_f.data['ck'];
const k_cont = source_env.data['k'];
const env = source_env.data['val'];
// La amplitud de la sinc es directamente 'duty'
const PI = Math.PI;
// Stems
for (let i = 0; i < k.length; i++) {
let val_k = k[i];
if (val_k === 0) {
ck[i] = duty;
} else {
let arg = val_k * duty;
ck[i] = duty * Math.sin(PI * arg) / (PI * arg);
}
}
source_f.change.emit();
// Envolvente
for (let i = 0; i < k_cont.length; i++) {
let val_k = k_cont[i];
if (Math.abs(val_k) < 1e-5) {
env[i] = duty;
} else {
let arg = val_k * duty;
env[i] = duty * Math.sin(PI * arg) / (PI * arg);
}
}
source_env.change.emit();
// --- 4. TICK Y (C0) ---
source_ytick.data['y'][0] = duty;
source_ytick.change.emit();
lbl_c0.y = duty;
""")
s_T0.js_on_change('value', callback)
s_duty.js_on_change('value', callback)
# ==========================================
# 5. LAYOUT
# ==========================================
caption_text = """
<div style="font-family: sans-serif; margin-top: 10px; font-size: 14px; opacity: 0.8;">
<b>Onda rectangular periódica:</b>
<ul>
<li>Representación temporal.</li>
<li>Coeficientes de su serie de Fourier.</li>
</ul>
</div>
"""
caption = Div(text=caption_text)
layout = column(row(s_T0, s_duty), p_time, p_freq, caption, sizing_mode="scale_width")
show(layout)