import numpy as np
from bokeh.plotting import figure, show
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, CustomJS, Slider, RadioButtonGroup, Label, Arrow, NormalHead, Button, Div
from bokeh.io import output_notebook
# Importamos tus funciones
from utils.plot_helpers import style_math_axes, add_math_ticks
output_notebook()
# ==========================================
# 1. PARÁMETROS
# ==========================================
COLOR_SIG = 'blue'
COLOR_PHASE = 'blue'
COLOR_TREND = '#999999'
# Valores Iniciales
VAL_TD_INIT = 0.0
VAL_SEP_INIT = 2.0
VAL_SCALE_INIT = 1.0
WIDTH_BASE = 1.0
width = WIDTH_BASE * VAL_SCALE_INIT
dist = VAL_SEP_INIT * width
T_period = 2 * dist
w0 = 2 * np.pi / T_period
RANGO_T = 15.0
K_MAX = 15
k_indices = np.arange(-K_MAX, K_MAX + 1)
# ==========================================
# 2. DATOS INICIALES
# ==========================================
t_vals = np.linspace(-RANGO_T, RANGO_T, 1000)
def get_y_time_scaled(t_arr, w_base, d_base, td, sc):
y = np.zeros_like(t_arr)
w_current = w_base * sc
for k_pulse in range(-15, 16):
center = sc * (k_pulse * d_base + td)
sign = 1 if k_pulse % 2 == 0 else -1
val = np.maximum(0, 1 - np.abs(t_arr - center)/w_current)
y += sign * val
return y
y_vals = get_y_time_scaled(t_vals, WIDTH_BASE, dist, 0, VAL_SCALE_INIT)
y_ghost = get_y_time_scaled(t_vals, WIDTH_BASE, dist, 0, VAL_SCALE_INIT)
source_sig = ColumnDataSource(data=dict(t=t_vals, y=y_vals))
source_ghost = ColumnDataSource(data=dict(t=t_vals, y=y_ghost))
source_marker = ColumnDataSource(data=dict(x=[0], y=[0], text=["td"]))
source_dim = ColumnDataSource(data=dict(
x_start=[0], y_start=[1.15],
x_end=[T_period], y_end=[1.15]
))
# --- FRECUENCIA ---
mag_ck, phase_ck, phase_trend, real_ck, imag_ck = [], [], [], [], []
for k in k_indices:
phase_trend.append(0)
if k % 2 == 0:
m, p, r, i_val = 0, 0, 0, 0
else:
arg = k * np.pi / (2 * VAL_SEP_INIT)
sinc = np.sin(arg)/arg if abs(arg)>1e-9 else 1.0
scale_amp = 1.0 / VAL_SEP_INIT
m = abs((sinc**2) * scale_amp)
p, r, i_val = 0, m, 0
mag_ck.append(m)
phase_ck.append(p)
real_ck.append(r)
imag_ck.append(i_val)
source_freq = ColumnDataSource(data=dict(
k=k_indices, mag=mag_ck, phase=phase_ck, phase_trend=phase_trend,
re=real_ck, im=imag_ck, zeros=np.zeros_like(k_indices)
))
# ==========================================
# 3. GRÁFICOS
# ==========================================
# --- A. TIEMPO ---
p_time = figure(width=600, height=300)
style_math_axes(p_time, x_range=(-RANGO_T, RANGO_T), y_range=(-1.2, 1.6),
xlabel="t", ylabel=r"$$\tilde{x}(\alpha t - t_d)$$")
add_math_ticks(p_time, yticks=[-1, 1], ytick_labels=["-1", "1"], tick_len=10)
p_time.line('t', 'y', source=source_ghost, line_dash='dashed', color=COLOR_SIG, alpha=0.3, line_width=1.5)
p_time.line('t', 'y', source=source_sig, color=COLOR_SIG, line_width=2.5)
p_time.segment(x0='x', y0=-0.1, x1='x', y1=0.1, source=source_marker, color="black", line_width=1)
p_time.text(x='x', y=-0.25, text='text', source=source_marker, text_align='center', text_font_style='italic')
# Cota T0
arrow_head = NormalHead(fill_color="black", size=8, line_color="black")
p_time.add_layout(Arrow(end=arrow_head, x_start='x_start', y_start='y_start', x_end='x_end', y_end='y_end',
source=source_dim, line_color="black", line_width=1))
p_time.add_layout(Arrow(end=arrow_head, x_start='x_end', y_start='y_end', x_end='x_start', y_end='y_start',
source=source_dim, line_color="black", line_width=1))
lbl_t0 = Label(x=T_period/2, y=1.25, text=r"$$T_0$$", text_align='center', text_baseline='bottom', text_color="black")
p_time.add_layout(lbl_t0)
# --- B. FRECUENCIA ---
# 1. MAGNITUD
p_mag = figure(width=300, height=200, name="plot_polar")
style_math_axes(p_mag, x_range=(-K_MAX-1, K_MAX+1), y_range=(0, 0.6), xlabel="k", ylabel=r"$$|c_k|$$")
p_mag.segment(x0='k', y0='zeros', x1='k', y1='mag', source=source_freq, color=COLOR_SIG, line_width=2)
p_mag.scatter('k', 'mag', source=source_freq, color=COLOR_SIG, size=6, marker="circle")
# 2. FASE
p_phase = figure(width=300, height=200, name="plot_polar")
style_math_axes(p_phase, x_range=(-K_MAX-1, K_MAX+1), y_range=(-3.5, 3.5), xlabel="k", ylabel=r"$$\angle c_k$$")
# Ticks Fase: -pi, -pi/2, pi/2, pi (sin el 0)
add_math_ticks(p_phase,
yticks=[-np.pi, -np.pi/2, np.pi/2, np.pi],
ytick_labels=[r"$$-\pi$$", r"$$-\frac{\pi}{2}$$", r"$$\frac{\pi}{2}$$", r"$$\pi$$"],
tick_len=10)
p_phase.line([-100, 100], [np.pi, np.pi], line_dash='dashed', color='gray', alpha=0.3)
p_phase.line([-100, 100], [-np.pi, -np.pi], line_dash='dashed', color='gray', alpha=0.3)
p_phase.line('k', 'phase_trend', source=source_freq, line_dash='dotted', color=COLOR_TREND, alpha=0.8, line_width=1.5)
p_phase.segment(x0='k', y0='zeros', x1='k', y1='phase', source=source_freq, color=COLOR_PHASE, line_width=2)
p_phase.scatter('k', 'phase', source=source_freq, color=COLOR_PHASE, size=6, marker="circle")
# 3. REAL / IMAG (Ocultos)
p_real = figure(width=300, height=200, name="plot_rect", visible=False)
style_math_axes(p_real, x_range=(-K_MAX-1, K_MAX+1), y_range=(-0.6, 0.6), xlabel="k", ylabel=r"$$Re\{c_k\}$$")
p_real.segment(x0='k', y0='zeros', x1='k', y1='re', source=source_freq, color=COLOR_SIG, line_width=2)
p_real.scatter('k', 're', source=source_freq, color=COLOR_SIG, size=6, marker="circle")
p_imag = figure(width=300, height=200, name="plot_rect", visible=False)
style_math_axes(p_imag, x_range=(-K_MAX-1, K_MAX+1), y_range=(-0.6, 0.6), xlabel="k", ylabel=r"$$Im\{c_k\}$$")
p_imag.segment(x0='k', y0='zeros', x1='k', y1='im', source=source_freq, color=COLOR_PHASE, line_width=2)
p_imag.scatter('k', 'im', source=source_freq, color=COLOR_PHASE, size=6, marker="circle")
# ==========================================
# 4. INTERACCIÓN
# ==========================================
s_td = Slider(start=0, end=5.0, value=VAL_TD_INIT, step=0.1, title=r"Desplazamiento (td)")
s_sep = Slider(start=2.0, end=6.0, value=VAL_SEP_INIT, step=0.1, title=r"Separación (Periodo/Ancho)")
s_scale = Slider(start=0.5, end=2.0, value=VAL_SCALE_INIT, step=0.1, title="Escala Temporal (α)")
view_selector = RadioButtonGroup(labels=["Magnitud / Fase", "Real / Imaginaria"], active=0)
btn_reset = Button(label="Reset Parámetros", button_type="default", width=150)
# Callback Reset
callback_reset = CustomJS(
args=dict(s_td=s_td, s_sep=s_sep, s_scale=s_scale,
v_td=VAL_TD_INIT, v_sep=VAL_SEP_INIT, v_scale=VAL_SCALE_INIT),
code="""
s_td.value = v_td;
s_sep.value = v_sep;
s_scale.value = v_scale;
""")
btn_reset.js_on_event('button_click', callback_reset)
# Callback Cálculo
callback_calc = CustomJS(
args=dict(source_sig=source_sig, source_ghost=source_ghost,
source_marker=source_marker, source_freq=source_freq,
source_dim=source_dim, lbl_t0=lbl_t0,
s_td=s_td, s_sep=s_sep, s_scale=s_scale),
code="""
const td = s_td.value;
const sep_factor = s_sep.value;
const scale = s_scale.value;
// FISICA BASE
const WIDTH_BASE = 1.0;
const DIST_BASE = sep_factor * WIDTH_BASE;
const T_PERIOD_BASE = 2 * DIST_BASE;
const W0_BASE = 2 * Math.PI / T_PERIOD_BASE;
const width_curr = WIDTH_BASE * scale;
// 1. TIEMPO
const t = source_sig.data['t'];
const y = source_sig.data['y'];
const y_g = source_ghost.data['y'];
for(let i=0; i<t.length; i++) { y[i]=0; y_g[i]=0; }
for (let k = -12; k <= 12; k++) {
let sign = (Math.abs(k) % 2 === 0) ? 1 : -1;
let center = scale * (k * DIST_BASE + td);
let center_g = scale * (k * DIST_BASE);
for (let i = 0; i < t.length; i++) {
let val = 1 - Math.abs(t[i] - center) / width_curr;
if (val > 0) y[i] += sign * val;
let val_g = 1 - Math.abs(t[i] - center_g) / width_curr;
if (val_g > 0) y_g[i] += sign * val_g;
}
}
source_sig.change.emit();
source_ghost.change.emit();
source_marker.data['x'][0] = td * scale;
source_marker.change.emit();
// 2. COTA T0
const x0 = scale * td;
const x1 = scale * (2 * DIST_BASE + td);
source_dim.data['x_start'][0] = x0;
source_dim.data['x_end'][0] = x1;
lbl_t0.x = (x0 + x1) / 2;
// 3. FRECUENCIA
const k_arr = source_freq.data['k'];
const mag = source_freq.data['mag'];
const ph = source_freq.data['phase'];
const ph_trend = source_freq.data['phase_trend'];
const re = source_freq.data['re'];
const im = source_freq.data['im'];
const two_pi = 2 * Math.PI;
const scale_amp = 1.0 / sep_factor;
for (let i = 0; i < k_arr.length; i++) {
let k = k_arr[i];
let angle = -k * W0_BASE * td;
let wrapped = ((angle + Math.PI) % two_pi);
if (wrapped < 0) wrapped += two_pi;
wrapped -= Math.PI;
ph_trend[i] = wrapped;
if (k % 2 === 0) {
mag[i]=0; ph[i]=0; re[i]=0; im[i]=0;
} else {
let arg = k * Math.PI / (2 * sep_factor);
let sinc_val = (Math.abs(arg) < 1e-9) ? 1.0 : Math.sin(arg)/arg;
let m = (sinc_val * sinc_val) * scale_amp;
mag[i] = m;
re[i] = m * Math.cos(angle);
im[i] = m * Math.sin(angle);
if (m < 1e-5) ph[i] = 0;
else ph[i] = wrapped;
}
}
source_freq.change.emit();
""")
s_td.js_on_change('value', callback_calc)
s_sep.js_on_change('value', callback_calc)
s_scale.js_on_change('value', callback_calc)
callback_view = CustomJS(
args=dict(p_mag=p_mag, p_phase=p_phase, p_real=p_real, p_imag=p_imag),
code="""
if (cb_obj.active === 0) {
p_mag.visible = true;
p_phase.visible = true;
p_real.visible = false;
p_imag.visible = false;
} else {
p_mag.visible = false;
p_phase.visible = false;
p_real.visible = true;
p_imag.visible = true;
}
""")
view_selector.js_on_change('active', callback_view)
# ==========================================
# 5. CAPTION
# ==========================================
texto_caption = """
<div style="font-family: sans-serif; margin-top: 10px; font-size: 14px; opacity: 0.8;">
<b>Propiedades de la Serie de Fourier.</b> Desplazamiento en el tiempo, simetría y conjugación, y escalado temporal.
</div>
"""
caption = Div(text=texto_caption)
# ==========================================
# 6. LAYOUT
# ==========================================
controls = column(s_td, s_sep, s_scale, btn_reset, view_selector)
freq_row = row(p_mag, p_phase, p_real, p_imag)
layout = column(controls, p_time, freq_row, caption, sizing_mode="scale_width")
show(layout)