import numpy as np
import panel as pn
import holoviews as hv
from scipy.stats import chisquare
pn.extension()
hv.extension('bokeh')
# Widgets
amplitude = pn.widgets.FloatSlider(name='Amplitude', start=0, end=10, step=0.1, value=1)
center = pn.widgets.FloatSlider(name='Center', start=-5, end=5, step=0.1, value=0)
width = pn.widgets.FloatSlider(name='Width', start=0.1, end=5, step=0.1, value=1)
noise_std = pn.widgets.FloatSlider(name='Noise Std Dev', start=0.01, end=2, step=0.01, value=0.2)
binning_factor = pn.widgets.IntSlider(name='Binning Factor', start=1, end=100, step=1, value=1)
n_points = pn.widgets.IntSlider(name='Number of Points', start=200, end=20000, step=100, value=200)
generate_button = pn.widgets.Button(name='Generate New Dataset', button_type='primary')
# NEW: toggle to show/hide the model curve
show_model = pn.widgets.Toggle(name='Show Model', value=True, button_type='success')
# State
x_vals = None
data_y = None
binned_x = None
binned_y = None
binned_err = None
original_noise_std = None
# Functions
def gaussian(x, amp, cen, wid):
return amp * np.exp(-0.5 * ((x - cen) / wid)**2)
def simulate(event=None):
global data_y, original_noise_std, x_vals
x_vals = np.linspace(-5, 5, n_points.value)
y_model = gaussian(x_vals, amplitude.value, center.value, width.value)
noise = np.random.normal(0, noise_std.value, size=len(x_vals))
data_y = y_model + noise
original_noise_std = noise_std.value
update_plot()
def compute_binned_data():
global binned_x, binned_y, binned_err
factor = binning_factor.value
if factor == 1 or data_y is None:
binned_x, binned_y, binned_err = x_vals, data_y, np.full_like(data_y, original_noise_std)
return
n_bins = len(x_vals) // factor
x_reshape = x_vals[:n_bins * factor].reshape(n_bins, factor)
y_reshape = data_y[:n_bins * factor].reshape(n_bins, factor)
binned_x = x_reshape.mean(axis=1)
binned_y = y_reshape.mean(axis=1)
binned_err = y_reshape.std(axis=1) / np.sqrt(factor)
def calculate_pvalue():
mean = np.mean(binned_y)
residuals = binned_y - mean
chi2_stat = np.sum((residuals / binned_err)**2)
dof = len(binned_y) - 1
pval = 1 - chisquare(f_obs=binned_y, f_exp=np.full_like(binned_y, mean))[1]
return pval
def update_plot(event=None):
compute_binned_data()
pval = calculate_pvalue()
pval_text = f"P-value (null = noise around mean): {pval:.3g}"
elements = []
# Optional: original unbinned data if binning > 1
if binning_factor.value > 1 and x_vals is not None and data_y is not None:
orig = hv.Scatter((x_vals, data_y)).opts(alpha=0.1, size=4, color='gray')
elements.append(orig)
# Binned data + error bars
scatter = hv.Scatter((binned_x, binned_y), label='Binned Data').opts(size=6)
errorbars = hv.ErrorBars((binned_x, binned_y, binned_err)).opts(line_width=1.5)
elements.extend([scatter, errorbars])
# NEW: only add model curve if toggle is on
if show_model.value:
curve = hv.Curve(
(binned_x, gaussian(binned_x, amplitude.value, center.value, width.value)),
label='Model'
)
elements.append(curve)
plot = hv.Overlay(elements).opts(
width=700,
height=400,
show_legend=True,
xlabel='x',
ylabel='y'
)
plot_pane.object = plot
pval_pane.object = f"**{pval_text}**"
# UI
plot_pane = pn.pane.HoloViews()
pval_pane = pn.pane.Markdown("")
generate_button.on_click(simulate)
binning_factor.param.watch(update_plot, 'value')
show_model.param.watch(update_plot, 'value') # NEW: re-render when toggled
controls = pn.Column(
amplitude, center, width, noise_std,
binning_factor, n_points,
generate_button,
show_model, # NEW: add toggle to the controls
pval_pane
)
layout = pn.Row(controls, plot_pane)
simulate() # Initialize with one dataset
layout.servable()