File size: 10,277 Bytes
1539f5b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2f02b79
1539f5b
d8658ea
2f02b79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4cadf36
ca42357
4cadf36
1539f5b
 
 
 
 
d8658ea
2fbd96d
1539f5b
 
 
 
 
 
 
 
 
 
ca42357
1539f5b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
from matplotlib import pyplot as plt
import numpy as np
from collections import Counter,deque
import time
import panel as pn
import holoviews as hv
import random


class CustomDeque(deque):
    def __init__(self, maxlen=None):
        super().__init__(maxlen=maxlen)
    def appendleft(self, x):
        # Close the oldest figure before removing it
        if len(self) == self.maxlen:
            plt.close(self[-1])
        super().appendleft(x)
    def append(self, x):
        # Close the oldest figure before removing it
        if len(self) == self.maxlen:
            plt.close(self[0])
        super().append(x)


global_prob = []
chart_history = CustomDeque(maxlen=4)

def get_dice_prob_bargraph(events_num,equal,color='#7a5af5'):
    global global_prob
    probabilities = create_distribution(events_num = events_num, equal=equal)
    global_prob = probabilities
    if not np.isclose(sum(probabilities), 1):
        raise ValueError("Probabilities must sum to 1.")
    outcomes = np.arange(1,len(probabilities)+1,dtype=int)# the number of probabilities will tell us how many outcomes or sides of dice we have
    np.arange(1,len(probabilities)+1,dtype=int)
    fig, ax = setup_darkmode_axes(1.20,1.2)
    ax.bar(outcomes, probabilities, tick_label=outcomes, color=color, alpha=0.7)
    ax.set_xticks(np.linspace(0, len(probabilities), len(probabilities)+1))
    ax.set_yticks(np.linspace(0, 1, 11))
    ax.tick_params(axis='x', colors='white',labelsize=4,pad=1)
    ax.tick_params(axis='y', colors='white',labelsize=4,pad=1)
    ax.set_title("Distribution of Sides",color="w",fontsize=5)
    ax.set_xlabel("Number of Sides",color="w",fontsize=6,labelpad=2)
    ax.set_ylabel("Probability",color="w",fontsize=6,labelpad=2)
    return fig


def roll_weighted_die(probabilities):
    if not np.isclose(sum(probabilities), 1): 
        raise ValueError("Probabilities must sum to 1.")
    # Define the possible outcomes of the die roll
    outcomes = np.arange(1,len(probabilities)+1,dtype=int)# the number of probabilities will tell us how many outcomes or sides of dice we have
    # Sample from the distribution
    roll = np.random.choice(outcomes,p=probabilities)
    return roll

def make_dice_hist(bins,dice_num=50,events_num=6, fig=None, ax=None,color='y'):
    min_val = min(bins)
    max_val = max(bins) 
    first_time = False
    if fig is None:        
        fig, ax = setup_darkmode_axes(9, 2)
        ax.set_xlabel("Dice Sum",color="w",fontsize=6)
        ax.set_ylabel("Occured How Many Times",color="w",fontsize=6)
    ax.clear()
    ax.set_title(f"Dice Sum",color="w")
    #bin_edges = np.arange(min_val, max_val + 1.5) - 0.5  # Create bin edges the 1.5 will get the last bin in (it needs both left and right edges, and the -.5 will shift them all so they are 
    #bin_edges = np.arange(min_val, max_val + 2) - 0.5
    #centered around each bin starting 
    #ax.clear()
    #counts, bins, patches = ax.hist(bins,bins=bin_edges,density=False, alpha=0.8,rwidth=1, color='y', histtype='bar', orientation = 'vertical', edgecolor='w') # counts are the y or height of each bin,
    counts, bins, patches = ax.hist(bins,density=False, alpha=0.6,rwidth=1, color=color, histtype='bar', orientation = 'vertical', edgecolor='w') # counts are the y or height of each bin,
    # Set x-ticks to match the bin edges
    #bin_midpoints = bins[:-1] + np.diff(bins)/2 # for each bin left side except the last one (because that is the right side) add to it the difference of each pair of bins /2 (midpoint) thus storing
    # the midpoints of the bins 
   # ax.tick_params(axis='x', labelsize=6, colors="white")  # Adjust the font size here
   #ax.tick_params(axis='y', labelsize=6, colors="white")  # Adjust the font size here
    #ax.set_xticks(bin_midpoints[::2])  # put a tick at each mid point of each 10th' bin
    #ax.set_yticks(np.linspace(0, max(counts)*1.25, 3))
    return fig,ax,counts,bins

def create_distribution(events_num = 6, equal=True):
    # Generate random numbers
    if not equal: 
        random_numbers = np.random.random(events_num) # random numbers between 0-1 of length num_events to make a weighted distribution (not equal for all)
        # Normalize to get probabilities that add up to 1
        probabilities = random_numbers / np.sum(random_numbers)
    else:
        probabilities = [1/events_num] * events_num # 
    return probabilities

def create_sum(dice_num: int = 20,probabilities=[1/6]*6) -> list : 
    return sum(roll_weighted_die(probabilities) for _ in range(dice_num)) # roll the dice the number of times needed and sum it them up

    
def run_trials(trials_num = 10, dice_num: int =20,probabilities=[1/6]*6):
    sum_list= []
    fig = None
    ax = None
    for i in range(trials_num):
        sum_list.append(create_sum(dice_num,probabilities))
    # create/update our histogram
        (fig,ax,counts,bins) = make_dice_hist(sum_list,i+1,dice_num,len(probabilities),fig,ax)
        plt.show()
        time.sleep(.25)
    return sum_list 

def setup_darkmode_axes(fig_width = 6, fig_height= 2, grid_on = True):
    fig,  ax = plt.subplots(nrows = 1,figsize=(fig_width,fig_height),dpi=400,layout='compressed')
    fig.set_facecolor("#212529")
    ax.set_facecolor("black")
    if grid_on:
        ax.grid(color='white', linewidth=.6, alpha=0.5, zorder=0)
    else:
        ax.set_axis_off()
    return fig,ax

def update_hex_value(event):
    print('updating color picker hex value')
    color_picker_hex.value = event.new

def show_chart_history(running,local_chart_history = None):
    print(f'show_chart_history called chart history {len(chart_history)}\n')
    if not running:
        return
    col_layout = pn.Column()
    # Add each figure as a separate panel to the row layout
    for fig in local_chart_history:
        panel = pn.pane.Matplotlib(fig, dpi=400, format="svg",sizing_mode="stretch_width")
        col_layout.append(panel)
    return col_layout


def run_simulation(running,num_sums=50,num_dice=30,color='yellow'):
    run_input.disabled = True  # Disable the button
    global global_prob,chart_history
    fig = None
    ax = None
    try:
        num_sides = len(global_prob)
        if not running:
            return # if the panel isn't active just return
        sum_list= []
        start_time = time.time()
        for i in range(num_sums): 
            loop_start = time.time()
            start_time = time.time()
            sum_list.append(create_sum(num_dice,global_prob))
            end_time = time.time()
            elapsed_time = end_time - start_time
           # print(f"sum list took {elapsed_time:.4f}")   
        
            start_time = time.time()
            if not (i+1) % 50: # only update every 10th sum
                start_time = time.time()  # Record the start time
                (fig,ax,counts,bins) = make_dice_hist(sum_list,20,len(global_prob),fig,ax,color)
                ax.set_title(f"Dice Sum #{i+1}/{num_sums} using {num_dice} {num_sides}-sided dice",color="w")
                ax.set_xlabel("Dice Sum",color="w",fontsize=6)
                ax.set_ylabel("Occured How Many Times",color="w")
                ax.tick_params(axis='x', labelsize=5, colors="w",pad=1)  # Adjust the font size here
                ax.tick_params(axis='y', labelsize=5, colors="w",pad=1)  # Adjust the font size here
                ax.grid(color='white', linewidth=.6, alpha=0.2, zorder=0)
                end_time = time.time()
                elapsed_time = end_time - start_time
               # print(f"Make dice hist took {elapsed_time:.4f}")
                start_time = time.time()  # Record the start time
                layout=pn.pane.Matplotlib(fig, dpi=400, format="svg",sizing_mode="stretch_width")
                end_time = time.time()
                elapsed_time = end_time - start_time
           # print(f"Setting the layout to fig took {elapsed_time:.4f}")
                yield layout
                time.sleep(.2)
            #time.sleep(.15)
            end_time = time.time()
            elapsed_time = end_time - start_time      
           # print(f"Yield took {elapsed_time:.4f}")
            loop_end = time.time()
            elapsed_time = loop_end - loop_start
        yield layout

           # print(f"Loop took {elapsed_time:.4f}\n\n")
    finally:
        run_input.disabled = False  # Re-enable the button after the simulation
        if fig:
            chart_history.appendleft(fig)
color_picker = pn.widgets.ColorPicker(name='Bar Color', value='#5e43f3')
color_picker_hex = pn.widgets.StaticText(name='Hex Value', value=color_picker.value)
str_pane1 = pn.pane.Str('Sides Probability Equal?', width=110)
random_switch = pn.widgets.Switch(name='Switch1', value=True, width=25)
num_events = pn.widgets.IntSlider(name='Number of Sides',start=2, end=12,step=1,value=6,width=150)
num_sums = pn.widgets.IntSlider(name='Run For How Long',start=50, end=20000,step=10,value=50,width=150)
num_dice = pn.widgets.IntSlider(name='Number of Dice (size of sum)',start=1, end=100,step=1,value=2,width=150)

#bind a plot to different params
bplot = pn.bind(get_dice_prob_bargraph, events_num=num_events, equal=random_switch,color=color_picker)
layout = pn.pane
# Attach the callback function to the color_picker's value parameter
color_picker.param.watch(update_hex_value, 'value')  # Define callback function to update plots
pn.extension(design='material',theme='dark',sizing_mode="stretch_width")
run_input = pn.widgets.Button(name="Run Simulation",icon="hand-click")



pn.template.MaterialTemplate(
        title=f'Visualizing The Central Limit Theorm',
        site="Statistics Playground",
        header_background="#1a1625",
        sidebar=['dice.png',num_events,str_pane1,random_switch,bplot,num_dice,num_sums,color_picker,run_input,
                pn.Row('gaussian_dist.png')],
        main=[pn.Card(pn.bind(run_simulation,run_input,num_sums=num_sums,num_dice=num_dice,color=color_picker),
                title="Current Sim", sizing_mode='stretch_width',styles={'background': '#5e5a66'}),
             pn.Card(pn.bind(show_chart_history,run_input,local_chart_history=chart_history),title="History (newest first)",sizing_mode='stretch_width',styles={'background': '#5e5a66'})
             ],
        sidebar_width=210,
    ).servable()