# The Python Code for The Double Slit Experiment Visualization
# ------------------------------------------------------------
# using the Bokeh library http://bokeh.org/
# by Jana Legerská
# jana.legerska@matfyz.cuni.cz
#-------------------------------------------------------------
import numpy as np
from numpy import math
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, CustomJS, Button, RadioButtonGroup, Slider
from bokeh.models import CheckboxButtonGroup, Panel, Tabs, RadioGroup, CheckboxGroup, Div
from bokeh.plotting import figure, show, output_file
from bokeh.models import NumeralTickFormatter
# With Young interferention only, no difraction
max_x = 30 # Screen length [cm]
var_x = 2 # Gaussian distribution variance
slitdist_factor = 100 # Ratio for exaggerating Gaussian side shift
slitdist = 500 # Slit distance [nm]
slitwidth = 500 # Slit width
wavelength = 100 # Wavelength [nm]
color = "red" # Initial particle traces colour
L = 20 # distance from source to the screen (L >> slitdist) [cm]
source = ColumnDataSource(data=dict(x=[], y=[], colors=[]), tags=[dict(max_x=max_x, slitdist_factor=slitdist_factor,
macro_scale=True, # the applet starts with interference
slitwidth=slitwidth, L=L, color=color)])
# Screen with points
p = figure(
title="Screen of the double slit experiment",
tools="", # (de)activate Bokeh tools
sizing_mode="stretch_width",
width=800,
height=380,
y_axis_label=None,
x_range=(-max_x,max_x),
y_range=(0, 1)
)
# Delete y-axis ticks on screen
p.yaxis.minor_tick_in = 0
p.yaxis.minor_tick_out = 0
p.yaxis.major_tick_in = 0
p.yaxis.major_tick_out = 0
p.yaxis.major_label_text_color = "white"
p.xaxis.major_label_text_color = "white"
p.yaxis[0].formatter = NumeralTickFormatter(format="0.00")
# Deactivate Bokeh logo
p.toolbar.logo = None
p.circle('x', 'y', source=source, size=7, fill_color='colors', line_color="black")
# Probability denstity function above the screen + histogram
pd = figure(title="Histogram and probability density",
sizing_mode="stretch_width",
width=p.width, height=250, x_range=p.x_range, tools="")
pd.toolbar.logo = None
source_pd = ColumnDataSource(data=dict(x=[], y=[]),
tags=[dict(var_x=var_x, previous_num=0)])
# previous_num ~ the number of particles sent before the change of distribution
pd_line = pd.line('x', 'y', source=source_pd, line_width=2, line_color="navy", visible=False)
# Delete y-axis ticks on pdf graph
pd.yaxis.minor_tick_in = 0
pd.yaxis.minor_tick_out = 0
pd.yaxis.major_tick_in = 0
pd.yaxis.major_tick_out = 0
pd.xaxis.major_label_text_color = "white"
pd.yaxis[0].formatter = NumeralTickFormatter(format="0.00")
# Histogram -- Young double slit
Nbins = 60 # number of bins
binwidth = 2*max_x/Nbins
# hist, bins = np.histogram([], range=(0, max_x), bins=Nbins)
# hist = relative frequancies
# bins = array array of division points of bins
# counts = absolute frequencies
source_hist = ColumnDataSource(data=dict(hist=[0 for _ in range(Nbins)],
bins=[binwidth/2 + i*binwidth - max_x for i in range(Nbins)], # bins centering
counts=[0 for _ in range(Nbins)], # array of absolute frequencies
widths = [binwidth for _ in range(Nbins)] # columns widths
)
)
pd.vbar(top="hist", x="bins", width="widths", source=source_hist, alpha=0.6, color="skyblue")
#------------------------------------------------------------------------------------------------
# Buttons definition
# Add 1 particle
button = Button(label="Send 1 particle", button_type="success")
# Send 100 particles at once
button100 = Button(label="Send 100 particles", button_type="success")
# Reset the screen
resetbutton = Button(label="Reset", button_type="success")
# Continuous stream of particles
streamtoggle = Button(label="Send continuous stream", button_type="success")
# Change distribution
LABELS = ["Uniform", "Normal", "Young"]
button_change_dist = RadioButtonGroup(labels=LABELS, active=0)
# Block/open slits
LABELS = ["Slit 1", "Slit 2"]
button_choose_slit = CheckboxButtonGroup(labels=LABELS, active=[0, 1])
# Detector
button_detector = Button(label="Detector", button_type="default")
# Sliders
slider_slitdist = Slider(start=0, end=1000, value=slitdist, step=10, title=None)
slider_slitwidth = Slider(start=0, end=5, value=0.1, step=0.1, title="Slit width")
slider_wavelength = Slider(start=100, end=1000, value=wavelength, step=10, title=None)
# Text -- slider labels
label_slitdist = Div(text="Slit distance: {} [nm]".format(slitdist), width=200, height=10)
label_wavelength = Div(text="Wavelength: {} [nm]".format(wavelength), width=200, height=10)
label_energy = Div(text="Energy: {} [eV]".format(round((6.626*3)/(wavelength)/1.602*100, 1)), width=200, height=20)
label_screen = Div(text="", height=20, align='center') # x [cm]
# Choose particle colour
LABELS_color = ["Red", "Blue", "Green"]
color_button = RadioGroup(labels=LABELS_color, active=0)
# Keep previous measurements after set up is changed
button_keep = CheckboxGroup(labels=["Keep previous measurements"], active=[])
text_keep_previous = Div(text="Keeping previous measurements is only possible if the sreen scale does not change. \n When using a detector, the screen is zoomed to nanometers scale. ",
height=20, align='center', visible=False)
# Visibility of panels, theoretical PDF
check_visible_pdf = CheckboxGroup(labels=["Show theoretical probability density"], active=[])
check_visible_pd_panel = CheckboxGroup(labels=["Show histogram panel"], active=[0])
# -----------------------------------------------------------------------------
# JS functions definition
# Def function of normal distribution with parameters mean, var (to be read from sliders)
js_normalPDF = """
function randn(mean, variance) { // Box Muller Transform
let u = 0, v = 0;
while(u === 0) u = Math.random(); //Converting [0,1) to (0,1)
while(v === 0) v = Math.random();
let num = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2 * Math.PI * v );
num = num * variance + mean
if (num < -source.tags[0]['max_x']) {return randn()} // resample between -max_x and max_x (abandon values out of screen)
else if (source.tags[0]['max_x'] < num) {return randn();}
else {return num;}
}
"""
# Def function of Young double slit interference distribution
js_youngPDF = """
function youngPDF(x) { // PDF of Young interferention
const freq = Math.PI*slider_slitdist.value/(slider_wavelength.value*source.tags[0].L)
return Math.cos(freq*x)**2
}
let minI = -source.tags[0].max_x;
let N = 100;
function youngCDF(maxI) { // CDF of Young interferention
let integral = 0;
let delta = (maxI - minI)/N
for (let i = 0; i < N; i++) {
integral += youngPDF(delta/2 + i*delta + minI)*delta;
}
return integral
}
const sample = []; // array of divistion points of the x-axis (up to max_x, step 0.01)
for (let i = -source.tags[0].max_x; i < source.tags[0].max_x; i += 0.01) {
sample.push(i);
}
const sample_y = sample.map(youngCDF); // youngCDF function values = the division points of the y-axis
// Normalization of PDF to the length of shade (2*max_x)
const h = sample_y[sample_y.length-1] - sample_y[0];
let index;
function youngInverseCDF(t) { // inverse CDF of Young interferention
for (let i = 0; i < sample_y.length; i++) {
if (sample_y[i] > t) {
index = i;
break;
}
}
const x2 = sample[index];
const x1 = sample[index - 1];
const y1 = sample_y[index - 1]
const c = (x2 - x1)/(sample_y[index] + y1);
return c*t - c*y1 + x1;
}
"""
# Function to send one particle
js_send = """
function send(u, renormalize=true) {
if (u!=false) { //nezapo49t8v8 částice při zavřených štěrbinách
source.data.x.push(u)
source.data.y.push(Math.random())
source.data.colors.push(source.tags[0].color) // Read particle colour from source
source.change.emit() // update the data source with local changes
// add to histogram
let j = Math.floor((u + source.tags[0].max_x)/s_hist.data.widths[0]); // index of bin where a particle with coord. new_x is added
s_hist.data.counts[j]++;
if (renormalize) {normalizehist()};
}
}
// normalize histogram
function normalizehist() {
// normalization; number of particles = source.data.x.length
let totalarea = source.data.x.length*s_hist.data.widths[0]; // total number of particles: source.data.x.length
for (let i = 0; i < s_hist.data.hist.length; i++) {
s_hist.data.hist[i] = s_hist.data.counts[i]/totalarea;
}
s_hist.change.emit();
}
"""
# Generator of a random number from a given distribution (uniform / normal(max_x/2, var_x) / young)
js_generator = """
function generator() {
const slitdist_adjusted = slider_slitdist.value/source.tags[0].slitdist_factor
// variance adjusted to approximate the one slit diffraction peak by gaussian distribution
let var_gaussdiff = (slider_wavelength.value*source.tags[0].L)/(Math.PI*source.tags[0].slitwidth)
if (slit.active.length==2){
if (detector.button_type == "primary") {
if (Math.random()<0.5) {
return randn(-slitdist_adjusted, var_gaussdiff) // source_pd.tags[0].var_x
}
else {
return randn(slitdist_adjusted, var_gaussdiff)
}
}
else {
return youngInverseCDF(h*Math.random())
}
}
else if (slit.active.length==1){
if (slit.active[0]==0 ) {
return randn(-slitdist_adjusted, var_gaussdiff)
}
else {
return randn(slitdist_adjusted, var_gaussdiff)
}
}
else {
return false
}
}
"""
# Function for reseting the screen
js_reset = """
function reset(resetpdf=true) { // no parameter => reset all
// reset data in source
source.data.x = []
source.data.y = []
source.data.colors = []
source.change.emit()
// reset histogram
for (let i = 0; i < s_hist.data.hist.length; i++) {
s_hist.data.hist[i] = 0;
s_hist.data.counts[i] = 0;
}
s_hist.change.emit()
if (resetpdf) { // parameter resetpdf=false => reset all except pdf
// reset pdf
source_pd.data.x = []
source_pd.data.y = []
source_pd.change.emit()
}
source_pd.tags[0].previous_num = 0
}
"""
#-------------------------------------------------------------------------
# JS CALLBACKS
js_functions = js_normalPDF + js_youngPDF + js_send + js_generator + js_reset + js_update_scale
js_cb_objects = dict(source=source, source_pd=source_pd, s_hist=source_hist,
streamtoggle=streamtoggle,
selected=button_change_dist, slit=button_choose_slit, detector=button_detector,
slider_slitdist=slider_slitdist, slider_slitwidth=slider_slitwidth,
slider_wavelength=slider_wavelength,
label_wavelength=label_wavelength, label_energy=label_energy, label_slitdist=label_slitdist,
color_button=color_button, button_keep=button_keep, text_keep_previous=text_keep_previous,
check_visible_pd_panel=check_visible_pd_panel, check_visible_pdf=check_visible_pdf,
screen=p, pd=pd, pd_line=pd_line,
screen_xaxis=p.xaxis, pd_xaxis=p.xaxis, label_screen=label_screen
)
# # # Theoretical pdf -- switch according to experiment set up
callback_Teorpdf = CustomJS(args=js_cb_objects, code="""
const max_x = source.tags[0].max_x
const slitdist = slider_slitdist.value
// slit distance to be displayed on the screen (not realistic)
const slitdist_adjusted = slider_slitdist.value/source.tags[0].slitdist_factor
// variance adjusted to approximate the one slit diffraction peak by gaussian distribution
// původně source_pd.tags[0].var_x
let var_gaussdiff = (slider_wavelength.value*source.tags[0].L)/(Math.PI*source.tags[0].slitwidth)
const wavelength = slider_wavelength.value
const envfreq = Math.PI*slitdist/(wavelength*source.tags[0].L)
// Normalization constant for the overall hist+pdf when button_keep active (not a pure distribution)
let keep_norm;
if (button_keep.active.length==0) {
keep_norm = 1
}
else { // ratio of particles from the actual distribution and all particles
keep_norm = (source.data.x.length - source_pd.tags[0].previous_num)/source.data.x.length
if (keep_norm==0) { // set nonzero normalization constatnt when no new particles added
keep_norm = 1
}
}
console.log("keep_norm:", keep_norm)
// Normalization constant for theoretical pdf: Young interference on the interval (-max_x; max_x)
const Norm = 2*envfreq/(envfreq*2*max_x + Math.sin(envfreq*2*max_x))
// Creating x.linspace(-max_x, max_x, 500)
if (source_pd.data.x.length==0) {
let step = 2*max_x/500;
for (let i = 0; i <= 500; i++) {
source_pd.data.x.push(i*step - max_x)}
}
const x = source_pd.data.x
let y;
if (slit.active.length==2) {
if (detector.button_type == "primary") { // Sum of both slits normal distributions
y = Array.from(x, (x) => keep_norm * (1/(Math.sqrt(2*Math.PI)*var_gaussdiff))/2 *
Math.exp(-0.5*((x + slitdist_adjusted)/var_gaussdiff)**2) +
keep_norm * (1/(Math.sqrt(2*Math.PI)*var_gaussdiff))/2 *
Math.exp(-0.5*((x - slitdist_adjusted)/var_gaussdiff)**2) )
}
else{ // Young interference
y = Array.from(x, (x) => keep_norm * Norm * (Math.cos(envfreq*x)**2) )
console.log(envfreq, y)
}
}
else if (slit.active.length==1) {
if (slit.active[0]==0) { // Left slit normal distribution
y = Array.from(x, (x) => keep_norm * (1/(Math.sqrt(2*Math.PI)*var_gaussdiff)) *
Math.exp(-0.5*((x + slitdist_adjusted)/var_gaussdiff)**2) )
}
else { // Right slit normal distribution
y = Array.from(x, (x) => keep_norm * (1/(Math.sqrt(2*Math.PI)*var_gaussdiff)) *
Math.exp(-0.5*((x - slitdist_adjusted)/var_gaussdiff)**2) )
}
}
else { // Both slits blocked, nothing
y = Array.from(x, (x) => 0 )
}
source_pd.data = { x, y }
source_pd.change.emit()
""")
callback = CustomJS(args=js_cb_objects, code=js_functions+"""
send(generator())
""")
callback100 = CustomJS(args=js_cb_objects, code=js_functions+"""
for (let i = 0; i < 100; i++) {
send(generator(), false)
}
normalizehist()
// console.log(selected.active)
console.log(source_pd.tags[0].previous_num)
""")
callback_reset = CustomJS(args=js_cb_objects, code=js_functions+"""
reset()
""")
callback_stream = CustomJS(args=js_cb_objects, code=js_functions+"""
function sendparticle() {
send(generator())
}
if (streamtoggle.button_type == "success") {
const interval = setInterval(sendparticle, 1000);
source.interval = interval;
streamtoggle.button_type = "danger";
streamtoggle.label = "Stop stream";
} else {
clearInterval(source.interval);
streamtoggle.button_type = "success";
streamtoggle.label = "Send continuous stream";
}
""")
# Block/open slits, change button color
callback_choose_slit = CustomJS(args=js_cb_objects, code=js_functions+ """
// update_scale()
// If Keep not checked, reset after changing slit set up
if (button_keep.active.length==0) {
reset()
}
else { // save actual number of particles sent from this distribution
source_pd.tags[0].previous_num = source.data.x.length
console.log(source_pd.tags[0].previous_num)
}
""")
# Switch detector active/no detector, change button color
callback_detector = CustomJS(args=js_cb_objects, code=js_functions+"""
// Change detector colour
if (detector.button_type == "default") {
detector.button_type = "primary";
detector.label = "Detector active";
} else {
detector.button_type = "default";
detector.label = "No detector present";
}
// update_scale()
// If Keep not checked, reset after switching on detector
if (button_keep.active.length==0) {
reset()
}
else { // save actual number of particles sent from this distribution
source_pd.tags[0].previous_num = source.data.x.length
console.log(source_pd.tags[0].previous_num)
}
""")
callback_change_slider = CustomJS(args=js_cb_objects, code=js_functions+ """
// Slider labels
label_wavelength.text = "Wavelenght: " + slider_wavelength.value + " [nm]"
label_energy.text = "Energy: " + ((6.626 * 3)/(slider_wavelength.value)/1.602*100).toFixed(1) + " [eV]"
label_slitdist.text = "Slit distance: " + slider_slitdist.value + " [nm]"
// If Keep not checked, reset after changing slider set up
if (button_keep.active.length==0) {
reset(false)
}
""")
# Choose partice colour
callback_choose_color = CustomJS(args=js_cb_objects, code=js_functions+"""
if (color_button.active==0) {
source.tags[0].color = "red"
console.log("red")
}
else if (color_button.active==1) {
source.tags[0].color = "#0073e6" // "#ffc857"
console.log("blue")
}
else {
source.tags[0].color = "#2eb82e" //"#2b9720"
console.log("green")
}
console.log(color_button.active)
// source.change.emit()
""")
callback_keep_text = CustomJS(args=js_cb_objects, code=js_functions+"""
if (button_keep.active.length==0) {
text_keep_previous.visible = false
}
else {
text_keep_previous.visible = true
}
""")
callback_visible_pd_panel = CustomJS(args=js_cb_objects, code=js_functions+"""
if (check_visible_pd_panel.active.length==0) {
pd.visible = false
}
else {
pd.visible = true
}
""")
callback_visible_pdf = CustomJS(args=js_cb_objects, code=js_functions+"""
if (check_visible_pdf.active.length==0) {
pd_line.visible = false
}
else {
pd_line.visible = true
}
""")
# -------------------------------------------------------------------------------
# Callbacks to objects
button.js_on_event('button_click', callback, callback_Teorpdf)
button100.js_on_event('button_click', callback100, callback_Teorpdf)
resetbutton.js_on_event('button_click', callback_reset)
streamtoggle.js_on_event('button_click', callback_stream, callback_Teorpdf)
button_choose_slit.js_on_click(callback_choose_slit)
button_choose_slit.js_on_click(callback_Teorpdf)
button_detector.js_on_event('button_click', callback_detector, callback_Teorpdf)
slider_slitdist.js_on_change('value', callback_Teorpdf, callback_change_slider)
slider_slitwidth.js_on_change('value', callback_Teorpdf, callback_change_slider)
slider_wavelength.js_on_change('value', callback_Teorpdf, callback_change_slider)
color_button.js_on_click(callback_choose_color)
check_visible_pd_panel.js_on_click(callback_visible_pd_panel)
check_visible_pdf.js_on_click(callback_visible_pdf)
layout = row(column(pd, p, label_screen),
column(button, button100, resetbutton, streamtoggle,
button_choose_slit, button_detector, # button_change_dist,
label_slitdist, slider_slitdist, # slider_slitwidth
label_wavelength, slider_wavelength, label_energy,
color_button,
check_visible_pd_panel, check_visible_pdf, button_keep, text_keep_previous))
output_file(filename="Applet_doubleslit88888.html", title="Applet double slit experiment")
show(layout)