ZainabEman commited on
Commit
00fb01e
Β·
verified Β·
1 Parent(s): 5f095ac

Upload 2 files

Browse files
Files changed (2) hide show
  1. Dockerfile +20 -0
  2. app.py +631 -0
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official Python base image
2
+ FROM python:3.10-slim
3
+
4
+ # Set working directory inside the container
5
+ WORKDIR /app
6
+
7
+ # Copy requirements file into the container
8
+ COPY requirements.txt .
9
+
10
+ # Install Python dependencies
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copy the rest of your app code into the container
14
+ COPY . .
15
+
16
+ # Expose Streamlit default port
17
+ EXPOSE 7860
18
+
19
+ # Command to run Streamlit app
20
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
app.py ADDED
@@ -0,0 +1,631 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import networkx as nx
5
+ import matplotlib.pyplot as plt
6
+ import seaborn as sns
7
+ import plotly.graph_objects as go
8
+ from hmmlearn import hmm
9
+ import plotly.express as px
10
+ from scipy.stats import kstest, expon
11
+ import io
12
+
13
+ # ──────────────────────────────────────────────────────────────────────────
14
+ # 1β€― Load cleaned data
15
+ # ──────────────────────────────────────────────────────────────────────────
16
+ @st.cache_data
17
+ def load_data():
18
+ return pd.read_csv("cleaned_slurm_log.csv") # adjust path if needed
19
+
20
+ df = load_data()
21
+
22
+ # ──────────────────────────────────────────────────────────────────────────
23
+ # 2β€― Sidebar navigation
24
+ # ──────────────────────────────────────────────────────────────────────────
25
+ page = st.sidebar.radio("Navigation", ["Home", "Markov", "Hidden Markov", "Queueing"])
26
+ st.sidebar.markdown("""
27
+ [πŸ“‚ View Source Code on GitHub](https://github.com/ZainabEman/MIT_supercloud-stochastic-analytics.git)
28
+ """)
29
+ # ──────────────────────────────────────────────────────────────────────────
30
+ # 3β€― Home tab
31
+ # ──────────────────────────────────────────────────────────────────────────
32
+ # ------------------------------------------------------------
33
+ # Home page
34
+ # ------------------------------------------------------------
35
+ if page == "Home":
36
+ import matplotlib.pyplot as plt
37
+
38
+ # ──────────────────────────────────────────────────────────
39
+ # Title & Subtitle
40
+ # ──────────────────────────────────────────────────────────
41
+ st.title("Stochastic Processes Project: Modeling GPU Resource Utilization")
42
+ st.caption("Markov ChainsΒ Β· HiddenΒ MarkovΒ ModelsΒ Β· Queueing Theory")
43
+
44
+ # ──────────────────────────────────────────────────────────
45
+ # Welcome Narrative (README‑style)
46
+ # ──────────────────────────────────────────────────────────
47
+ st.markdown(
48
+ """
49
+ ### πŸ‘‹ Welcome
50
+ This app documents our journey applying **stochastic‑process models** to a **2β€―TB GPU workload dataset** released by the [MITβ€―SuperCloud DatacenterΒ Challenge](https://supercloud.mit.edu/).
51
+ We focus on three lenses:
52
+
53
+ * **Markov Chains** β€” state transitions of GPU utilisation
54
+ * **Hidden Markov Models (HMMs)** β€” latent workload regimes
55
+ * **Queueing Theory** β€” arrival / service dynamics of SLURM jobs
56
+
57
+ ---
58
+
59
+ ### πŸ“‚ Dataset at a Glance
60
+ | Layer | Details |
61
+ |-------|---------|
62
+ | Root | `cpu/`Β andΒ `gpu/` |
63
+ | Sub‑folders | `0000/ … 0099/` (job shards) |
64
+ | Per‑job files | `*-summary.csv`Β +Β `*-timeseries.csv` |
65
+ | Total size | **β‰ˆβ€―2β€―TB** (public AWSΒ S3 bucket) |
66
+
67
+ We analysed a **representative GPU sample** to keep local storage humane.
68
+
69
+ *Dataset link β†’* **`s3://mit-ll-supercloud-dc/data/`**
70
+
71
+ ---
72
+
73
+ ### πŸ”‘ Extracted Features
74
+ * **Resource metrics**: `CPUUtilization`, `RSS`, `VMSize`, `IORead`, `IOWrite`, `Threads`, `ElapsedTime`
75
+ * **Job metadata**: `time_submit`, `time_start`, `time_end`, `state`, `cpus_req`, `mem_req`, `partition`
76
+
77
+ ---
78
+
79
+ ### πŸ—οΈ ChallengesΒ &Β Solutions
80
+ | Pain‑point | What we did |
81
+ |------------|-------------|
82
+ | **2β€―TB size** | `aws s3 cp --no-sign-request --recursive …` on only **gpu/** shards |
83
+ | **Undocumented states** | Combined SLURM codes + utilisation thresholds ➜ Idle / Normal / Busy |
84
+ | **Sparse symbols** | Emission matrices handle missing observations |
85
+ | **Validation** | Cross‑checked steady‑state vectors with domain intuition |
86
+
87
+ ---
88
+
89
+ ### πŸš€ Quick‑Start
90
+
91
+ ```bash
92
+ git clone https://github.com/ZainabEman/MIT_supercloud-stochastic-analytics.git
93
+ cd stocastiq
94
+ pip install -r requirements.txt
95
+ streamlit run app.py # loads a bundled sample file
96
+ ```
97
+ ## πŸ”— Live Repository
98
+
99
+ Browse the complete project source on GitHub:
100
+ **<https://github.com/ZainabEman/MIT_supercloud-stochastic-analytics.git>**
101
+
102
+ ---
103
+
104
+ ## πŸ“¦ Dataset Access
105
+
106
+ - **Publicβ€―AWSβ€―S3 bucket** (raw files, β‰ˆβ€―2β€―TB):
107
+ `s3://mit-ll-supercloud-dc/`
108
+
109
+ - **Dataset landing page / paper** (overview & citation):
110
+ <https://arxiv.org/abs/2106.09701>
111
+
112
+ ---
113
+
114
+ ## πŸŽ“ Acknowledgments
115
+
116
+ - **MITβ€―SuperCloud Datacenter Challenge** team for releasing the large‑scale workload dataset
117
+ - **SLURM** scheduler documentation and community contributors for elucidating job‑state codes
118
+ - Open‑source maintainers of **hmmlearn**, **streamlit**, and **matplotlib** for the libraries powering our analyses
119
+ - Everyone who reviewed the codebase, filed issues, or submitted pull requests to improve this project
120
+
121
+ """)
122
+
123
+
124
+ # ──────────────────────────────────────────────────────────────────────────
125
+ # 4β€― Markov‑chain analysis
126
+ # ──────────────────────────────────────────────────────────────────────────
127
+ elif page == "Markov":
128
+ st.title("Markov Chain Analysis (with Pending ↔ Running loops)")
129
+
130
+ st.subheader("Overview")
131
+ st.write(
132
+ """
133
+ States: **PENDING β†’ RUNNING β†’ [COMPLETED | FAILED | CANCELLED]**.
134
+ Some jobs retry, yielding **RUNNING β†’ PENDING β†’ RUNNING** loops before absorption.
135
+ """
136
+ )
137
+
138
+ # 4‑A Build empirical transition matrix ───────────────────────────────
139
+ states = ["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELLED"]
140
+ absorbing_set = {"COMPLETED", "FAILED", "CANCELLED"}
141
+ absorbing_list = [s for s in states if s in absorbing_set] # fixed order
142
+
143
+ counts = {s: {t: 0 for t in states} for s in states}
144
+ for trans_list in df["transitions"]:
145
+ for a, b in eval(trans_list):
146
+ if a in states and b in states:
147
+ counts[a][b] += 1
148
+
149
+ probs = {}
150
+ for s in states:
151
+ tot = sum(counts[s].values())
152
+ if s in absorbing_set:
153
+ probs[s] = {t: 1.0 if t == s else 0.0 for t in states}
154
+ else:
155
+ probs[s] = {t: counts[s][t] / tot if tot else 0.0 for t in states}
156
+
157
+ matrix_df = pd.DataFrame(probs).T
158
+ P = matrix_df.to_numpy()
159
+
160
+ st.subheader("Transition‑Probability Matrix")
161
+ st.table(matrix_df)
162
+
163
+ # 4‑B Heatmap ─────────────────────────────────────────────────────────
164
+ st.subheader("Heatmap")
165
+ fig_hm, ax = plt.subplots()
166
+ sns.heatmap(P, annot=True, fmt=".4f", cmap="Blues",
167
+ xticklabels=states, yticklabels=states, ax=ax)
168
+ st.pyplot(fig_hm)
169
+
170
+ # 4‑C State diagram ───────────────────────────────────────────────────
171
+ st.subheader("Empirical State Diagram")
172
+ G = nx.DiGraph()
173
+ for a in states:
174
+ for b in states:
175
+ if matrix_df.loc[a, b] > 0:
176
+ G.add_edge(a, b, weight=matrix_df.loc[a, b])
177
+
178
+ pos = nx.circular_layout(G)
179
+ node_colors = ["gold" if s == "PENDING"
180
+ else "skyblue" if s == "RUNNING"
181
+ else "lightcoral" for s in states]
182
+
183
+ node_trace = go.Scatter(
184
+ x=[pos[s][0] for s in states],
185
+ y=[pos[s][1] for s in states],
186
+ text=states,
187
+ mode="markers+text",
188
+ textposition="middle center",
189
+ marker=dict(size=55, color=node_colors, line=dict(width=2, color="black")),
190
+ hoverinfo="text"
191
+ )
192
+
193
+ def arrow(x0, y0, x1, y1, shift=0.0):
194
+ return dict(ax=x0+shift, ay=y0-shift, x=x1+shift, y=y1-shift,
195
+ xref="x", yref="y", axref="x", ayref="y",
196
+ showarrow=True, arrowhead=3, arrowwidth=2, arrowcolor="black")
197
+
198
+ arrows = []
199
+ for a, b in G.edges():
200
+ x0, y0 = pos[a]; x1, y1 = pos[b]
201
+ if {a, b} == {"PENDING", "RUNNING"}:
202
+ s = 0.04 if a == "PENDING" else -0.04
203
+ arrows.append(arrow(x0, y0, x1, y1, s))
204
+ else:
205
+ arrows.append(arrow(x0, y0, x1, y1))
206
+
207
+ st.plotly_chart(
208
+ go.Figure(
209
+ data=[node_trace],
210
+ layout=go.Layout(
211
+ annotations=arrows, showlegend=False, hovermode="closest",
212
+ xaxis=dict(visible=False), yaxis=dict(visible=False),
213
+ height=640, margin=dict(t=40, l=20, r=20, b=20),
214
+ title="State‑Transition Diagram"
215
+ )
216
+ )
217
+ )
218
+ st.markdown("πŸŸ‘β€―PENDINGβ€ƒβ€ƒπŸ”΅β€―RUNNINGβ€ƒβ€ƒπŸ”΄β€―Absorbing")
219
+
220
+ # 4‑D Fundamental matrix & metrics ────────────────────────────────────
221
+ idx = {s: i for i, s in enumerate(states)}
222
+ absorbing_idx = [idx[s] for s in absorbing_list]
223
+ transient_idx = [i for i in range(len(states)) if i not in absorbing_idx]
224
+
225
+ if transient_idx:
226
+ Q = P[np.ix_(transient_idx, transient_idx)]
227
+ try:
228
+ N = np.linalg.inv(np.eye(len(Q)) - Q) # fundamental
229
+ except np.linalg.LinAlgError:
230
+ N = None
231
+ else:
232
+ N = None
233
+
234
+ # Pre‑compute B = Nβ€―R and U = NΒ²β€―R (needed for per‑absorber times) ----
235
+ if N is not None:
236
+ R = P[np.ix_(transient_idx, absorbing_idx)]
237
+ B = N @ R # hitting probabilities to each absorber
238
+ U = (N @ N) @ R # unconditional time‑totals
239
+ else:
240
+ R = B = U = None
241
+
242
+ st.subheader("⏱️ Markov Calculations")
243
+
244
+ # (i)Β Recurrence & per‑absorber absorption -----------------------------
245
+ sel_state = st.selectbox("Choose a state", states, key="rec_sel")
246
+ i_sel = idx[sel_state]
247
+
248
+ colA, colB = st.columns(2)
249
+
250
+ with colA:
251
+ st.markdown("β€―Mean recurrence time")
252
+ if i_sel in absorbing_idx:
253
+ st.success("βˆžβ€―(absorbing)")
254
+ else:
255
+ if P[i_sel, i_sel] < 1:
256
+ st.success(f"{1/(1-P[i_sel,i_sel]):.4f}β€―steps")
257
+ else:
258
+ st.info("Self‑loopβ€―=β€―1 β†’ ∞")
259
+
260
+ with colB:
261
+ st.markdown("β€―Mean absorption time\n*(to each absorbing state)*")
262
+ if i_sel in absorbing_idx:
263
+ st.success("0β€―steps (already absorbing)")
264
+ elif N is None:
265
+ st.warning("Cannot compute (singular matrix).")
266
+ else:
267
+ row = transient_idx.index(i_sel)
268
+ for a_idx, a_name in zip(absorbing_idx, absorbing_list):
269
+ prob = B[row, absorbing_idx.index(a_idx)]
270
+ if prob == 0:
271
+ st.write(f"β€’ **{a_name}**: unreachableΒ (probβ€―0)")
272
+ else:
273
+ mean_k = U[row, absorbing_idx.index(a_idx)] / prob
274
+ st.write(f"β€’ **{a_name}**: {mean_k:.4f}β€―steps")
275
+
276
+ # (ii)Β Mean passage time srcβ€―β†’β€―tgt ------------------------------------
277
+ st.markdown("β€―Mean passage time between two states")
278
+ col1, col2 = st.columns(2)
279
+ with col1:
280
+ src_state = st.selectbox("From", states, key="pass_src")
281
+ with col2:
282
+ tgt_state = st.selectbox("To", states, key="pass_tgt")
283
+
284
+ if src_state == tgt_state:
285
+ st.info("Sourceβ€―=β€―target β†’ 0")
286
+ else:
287
+ P_mod = P.copy()
288
+ j_tgt = idx[tgt_state]
289
+ P_mod[j_tgt, :] = 0.0
290
+ P_mod[j_tgt, j_tgt] = 1.0
291
+ new_abs = absorbing_idx + ([] if j_tgt in absorbing_idx else [j_tgt])
292
+ new_trans = [k for k in range(len(states)) if k not in new_abs]
293
+ if new_trans:
294
+ Qm = P_mod[np.ix_(new_trans, new_trans)]
295
+ try:
296
+ Nm = np.linalg.inv(np.eye(len(Qm)) - Qm)
297
+ i_from = new_trans.index(idx[src_state])
298
+ meanpass = Nm[i_from, :].sum()
299
+ st.success(f"{meanpass:.4f}β€―steps")
300
+ except np.linalg.LinAlgError:
301
+ st.warning("Singular (Iβˆ’Q) – cannot compute.")
302
+ else:
303
+ st.info("All states absorbing under this modification.")
304
+
305
+ # 4‑E Hitting probability --------------------------------------------
306
+ st.markdown("---")
307
+ st.subheader("πŸŽ―β€―Long‑run hitting probability")
308
+
309
+ col3, col4 = st.columns(2)
310
+ with col3:
311
+ from_state = st.selectbox("From state", states, key="hit_from")
312
+ with col4:
313
+ to_state = st.selectbox("To state", states, key="hit_to")
314
+
315
+ if from_state == to_state:
316
+ st.success("1")
317
+ elif idx[from_state] in absorbing_idx:
318
+ st.info("0β€―(already in a different absorber)")
319
+ else:
320
+ if idx[to_state] in absorbing_idx and N is not None:
321
+ i = transient_idx.index(idx[from_state])
322
+ j = absorbing_idx.index(idx[to_state])
323
+ st.success(f"Probability: **{B[i,j]:.4f}**")
324
+ else:
325
+ P_tmp = P.copy()
326
+ j_tgt = idx[to_state]
327
+ P_tmp[j_tgt, :] = 0.0
328
+ P_tmp[j_tgt,j_tgt] = 1.0
329
+ abs_tmp = absorbing_idx + [j_tgt]
330
+ trans_tmp = [k for k in range(len(states)) if k not in abs_tmp]
331
+ Qh = P_tmp[np.ix_(trans_tmp, trans_tmp)]
332
+ Rh = P_tmp[np.ix_(trans_tmp, abs_tmp)]
333
+ try:
334
+ Nh = np.linalg.inv(np.eye(len(Qh)) - Qh)
335
+ Bh = Nh @ Rh
336
+ i = trans_tmp.index(idx[from_state])
337
+ j = abs_tmp.index(j_tgt)
338
+ st.success(f"Probability: **{Bh[i,j]:.4f}**")
339
+ except np.linalg.LinAlgError:
340
+ st.warning("Singular (Iβˆ’Q) – cannot compute.")
341
+
342
+ # ──────────────────────────────────────────────────────────────────────────
343
+ # 5β€― Placeholder pages
344
+ # ──────────────────────────────────────────────────────────────────────────
345
+ elif page == "Hidden Markov":
346
+ st.title("πŸ” Hidden Markov Model Analysis")
347
+
348
+ st.write("""
349
+ This tab applies a Hidden Markov Model (HMM) to the dataset to:
350
+ 1. Estimate steady-state probabilities
351
+ 2. Compute probability of observing a state sequence (Forward Algorithm)
352
+ 3. Infer the most likely hidden state sequence (Viterbi Algorithm)
353
+ """)
354
+
355
+ # βœ… Load CSV
356
+ df = pd.read_csv("timeseries.csv")
357
+
358
+ # βœ… Check if CPUFrequency exists
359
+ if 'CPUFrequency' not in df.columns:
360
+ st.error("Column 'CPUFrequency' not found in dataset!")
361
+ st.stop()
362
+
363
+ # βœ… Step 1: Preview
364
+ st.subheader("Step 1: Data Preview")
365
+ st.write(df[['ElapsedTime', 'CPUFrequency']].head())
366
+
367
+ # βœ… Step 2: Normalize CPUFrequency (0–100 scale)
368
+ df['CPUFrequency_normalized'] = df['CPUFrequency'] / df['CPUFrequency'].max() * 100
369
+
370
+ # βœ… Dynamic thresholds
371
+ q1 = df['CPUFrequency_normalized'].quantile(0.33)
372
+ q2 = df['CPUFrequency_normalized'].quantile(0.66)
373
+
374
+ def discretize(util):
375
+ if util < q1:
376
+ return 0 # Idle
377
+ elif util < q2:
378
+ return 1 # Normal
379
+ else:
380
+ return 2 # Busy
381
+
382
+ df['ObsState'] = df['CPUFrequency_normalized'].apply(discretize)
383
+
384
+ st.subheader("Step 2: Discretized Observed States")
385
+ st.write(df[['ElapsedTime', 'CPUFrequency_normalized', 'ObsState']].head())
386
+
387
+ # βœ… Check unique observed states
388
+ unique_states = np.unique(df['ObsState'])
389
+ st.write(f"Unique Observed States: {unique_states}")
390
+
391
+ # βœ… Inject synthetic samples if only 1 unique state
392
+ if len(unique_states) < 2:
393
+ st.warning("Only one observed state detected β†’ injecting synthetic samples for demonstration.")
394
+ df = pd.concat([df, pd.DataFrame({
395
+ 'ElapsedTime': [-1, -2],
396
+ 'CPUFrequency': [df['CPUFrequency'].max(), df['CPUFrequency'].min()],
397
+ 'CPUFrequency_normalized': [99, 1],
398
+ 'ObsState': [2, 0]
399
+ })], ignore_index=True)
400
+ unique_states = np.unique(df['ObsState'])
401
+ st.write(f"After injection β†’ Unique Observed States: {unique_states}")
402
+
403
+ # βœ… Observed sequence
404
+ observations = df['ObsState'].values.reshape(-1, 1)
405
+
406
+ # βœ… Fit HMM
407
+ from hmmlearn import hmm
408
+ n_components = 3
409
+ model = hmm.MultinomialHMM(n_components=n_components, n_iter=100, random_state=42)
410
+ model.fit(observations)
411
+
412
+ # βœ… Transition Matrix
413
+ st.subheader("Step 3: Transition Matrix (A)")
414
+ transmat_df = pd.DataFrame(model.transmat_, columns=[f"State {i}" for i in range(n_components)])
415
+ st.write(transmat_df)
416
+
417
+ # βœ… Emission Matrix
418
+ st.subheader("Step 4: Emission Probabilities (B)")
419
+ emission_probs = model.emissionprob_
420
+
421
+ cols = ["Idle", "Normal", "Busy"]
422
+ emiss_df = pd.DataFrame(0, index=[f"State {i}" for i in range(n_components)], columns=cols)
423
+
424
+ symbols_present = np.unique(df['ObsState'])
425
+ model_symbols = emission_probs.shape[1]
426
+
427
+ for symbol in symbols_present:
428
+ if symbol >= model_symbols:
429
+ st.warning(f"Symbol {symbol} not learned by HMM β†’ skipping assignment.")
430
+ continue
431
+ col_name = cols[symbol]
432
+ emiss_df[col_name] = emission_probs[:, np.where(np.unique(observations) == symbol)[0][0]]
433
+
434
+ st.write(emiss_df)
435
+
436
+ # βœ… Steady-State Probabilities
437
+ st.subheader("Step 5: Steady-State Probabilities")
438
+ eigvals, eigvecs = np.linalg.eig(model.transmat_.T)
439
+ steady_state = np.real(eigvecs[:, np.isclose(eigvals, 1)])
440
+ steady_state = steady_state[:, 0] / steady_state[:, 0].sum()
441
+ steady_df = pd.DataFrame(steady_state, index=[f"State {i}" for i in range(n_components)], columns=["Probability"])
442
+ st.write(steady_df)
443
+
444
+ # βœ… Forward Algorithm
445
+ st.subheader("Step 6: Forward Algorithm")
446
+ log_prob = model.score(observations)
447
+ st.write(f"Log Probability of observation sequence: {log_prob:.4f}")
448
+ st.write(f"Probability of observation sequence: {np.exp(log_prob):.6f}")
449
+
450
+ # βœ… Viterbi Algorithm
451
+ st.subheader("Step 7: Viterbi Algorithm (Most Likely Hidden States)")
452
+ hidden_states = model.predict(observations)
453
+ df['HiddenState'] = hidden_states
454
+ st.write(df[['ElapsedTime', 'CPUFrequency_normalized', 'ObsState', 'HiddenState']].head())
455
+
456
+ # βœ… Plot
457
+ import matplotlib.pyplot as plt
458
+ fig, ax = plt.subplots(figsize=(12, 4))
459
+ ax.plot(df['ElapsedTime'], df['CPUFrequency'], label='CPUFrequency')
460
+ ax.scatter(df['ElapsedTime'], df['HiddenState'] * df['CPUFrequency'].max() / 2,
461
+ color='red', label='Hidden State (scaled)')
462
+ ax.set_xlabel("ElapsedTime")
463
+ ax.set_ylabel("CPUFrequency / HiddenState")
464
+ ax.legend()
465
+ st.pyplot(fig)
466
+
467
+ # βœ… INTERPRETATION
468
+ st.subheader("Step 8: Interpretation of Results")
469
+ st.markdown("""
470
+ **πŸ“ Interpretation Summary:**
471
+
472
+ - The **Transition Matrix (A)** shows the probabilities of moving between hidden states (State 0, State 1, State 2).
473
+ For example, a high value on `State 0 β†’ State 0` means the system tends to stay idle.
474
+
475
+ - The **Emission Probabilities (B)** tell us how likely each hidden state emits an observed state (Idle, Normal, Busy).
476
+ For example, if `State 1` has a high probability for `Normal`, that state likely represents normal usage.
477
+
478
+ - The **Steady-State Probabilities** indicate the long-run percentage of time the system stays in each hidden state.
479
+ For example, a steady state of `State 2: 0.70` implies 70% of the time the CPU is busy.
480
+
481
+ - The **Log Probability** from the Forward Algorithm tells us how well the model explains the observed sequence:
482
+ higher values mean better fit.
483
+
484
+ - The **Viterbi Algorithm** outputs the most likely hidden state path for the data β†’ useful for inferring the CPU's operational mode over time.
485
+
486
+ - The final plot overlays the original CPU frequency and predicted hidden state sequence over time β†’ to visually compare how hidden states align with CPU activity.
487
+
488
+ βœ… **In simple terms: This analysis modeled how CPU usage fluctuates between hidden operational modes (idle, normal, busy) over time, and estimated how likely transitions and states are happening based on the data.**
489
+ """)
490
+
491
+
492
+
493
+ elif page == "Queueing":
494
+ import streamlit as st
495
+ import pandas as pd
496
+ import numpy as np
497
+ import plotly.express as px
498
+ from scipy.stats import kstest, expon
499
+ import io
500
+
501
+ st.title("πŸ“Š M/M/1 Queuing Theory Dashboard (Synthetic Data Mode)")
502
+ st.caption("Dataset: synthetic exponential data β†’ analysis in SECONDS")
503
+
504
+ # πŸ”Ή Synthetic data generator
505
+ @st.cache_data
506
+ def generate_synthetic_data(size=1000, interarrival_mean=60, service_mean=45):
507
+ np.random.seed(42)
508
+ interarrival_times = np.random.exponential(scale=interarrival_mean, size=size)
509
+ service_times = np.random.exponential(scale=service_mean, size=size)
510
+ return pd.DataFrame({
511
+ 'interarrival_time': interarrival_times,
512
+ 'service_time': service_times
513
+ })
514
+
515
+ df = generate_synthetic_data()
516
+
517
+ st.dataframe(df.head())
518
+
519
+ # πŸ”Ή Calculate Ξ» and ΞΌ
520
+ lam = 1 / df['interarrival_time'].mean()
521
+ mu = 1 / df['service_time'].mean()
522
+
523
+ # πŸ”Ή M/M/1 metrics function
524
+ def mm1_metrics(lam, mu, n=5, k=10):
525
+ rho = lam / mu
526
+ if rho >= 1:
527
+ return {"rho": rho}
528
+ L = rho / (1 - rho)
529
+ Lq = rho**2 / (1 - rho)
530
+ W = 1 / (mu - lam)
531
+ Wq = lam / (mu * (mu - lam))
532
+ P0 = 1 - rho
533
+ Pn = (1 - rho) * rho**n
534
+ Pgt_k = rho**(k + 1)
535
+ return dict(rho=rho, L=L, Lq=Lq, W=W, Wq=Wq, P0=P0, Pn=Pn, Pgt_k=Pgt_k)
536
+
537
+ metrics = mm1_metrics(lam, mu)
538
+
539
+ st.subheader("πŸ”’ M/M/1 Metrics (in SECONDS)")
540
+
541
+ if metrics.get("rho", 2) >= 1:
542
+ st.error(f"⚠️ System unstable (ρ = {metrics['rho']:.3f} β‰₯ 1). Metrics invalid.")
543
+ else:
544
+ c1, c2, c3, c4 = st.columns(4)
545
+ c1.metric("Ξ» (arrival rate)", f"{lam:.3f} /sec")
546
+ c2.metric("ΞΌ (service rate)", f"{mu:.3f} /sec")
547
+ c3.metric("ρ (utilization)", f"{metrics['rho']:.3f}", delta="⚠️" if metrics['rho'] > 0.9 else None)
548
+ c4.metric("Pβ‚€ (idle prob)", f"{metrics['P0']:.3f}")
549
+
550
+ c1, c2, c3, c4 = st.columns(4)
551
+ c1.metric("L (avg system)", f"{metrics['L']:.2f}")
552
+ c2.metric("Lq (avg queue)", f"{metrics['Lq']:.2f}")
553
+ c3.metric("W (time system)", f"{metrics['W']:.2f} sec")
554
+ c4.metric("Wq (wait time)", f"{metrics['Wq']:.2f} sec")
555
+
556
+ st.markdown(f"""
557
+ - **Pβ‚™ (n=5 customers):** {metrics['Pn']:.4f}
558
+ - **P(N > 10 customers):** {metrics['Pgt_k']:.4f}
559
+ """)
560
+
561
+ st.subheader("🧠 Interpretation")
562
+ if metrics['rho'] > 0.9:
563
+ st.warning("⚠️ Utilization exceeds 90% β†’ consider adding capacity.")
564
+ else:
565
+ st.success("βœ… System stable with moderate utilization.")
566
+
567
+ # πŸ”Ή KS p-value
568
+ def ks_pvalue(sample):
569
+ if len(sample) < 50:
570
+ return np.nan
571
+ mean = np.mean(sample)
572
+ return kstest(sample, expon(scale=mean).cdf).pvalue
573
+
574
+ pval_ia = ks_pvalue(df['interarrival_time'])
575
+ pval_sv = ks_pvalue(df['service_time'])
576
+
577
+ # πŸ”Ή Plots
578
+ st.subheader("πŸ“ˆ Interarrival Time Distributions")
579
+
580
+ fig_ia_lin = px.histogram(df, x='interarrival_time', nbins=50,
581
+ labels={"interarrival_time": "Interarrival time (sec)"},
582
+ title=f"Interarrival time (linear scale)\nKS p = {pval_ia:.4f}")
583
+ st.plotly_chart(fig_ia_lin, use_container_width=True)
584
+
585
+ st.subheader("πŸ“ˆ Service Time Distributions")
586
+
587
+ fig_sv_lin = px.histogram(df, x='service_time', nbins=50,
588
+ labels={"service_time": "Service time (sec)"},
589
+ title=f"Service time (linear scale)\nKS p = {pval_sv:.4f}")
590
+ st.plotly_chart(fig_sv_lin, use_container_width=True)
591
+ # πŸ”Ή KDE plots
592
+ st.subheader("πŸ“Š Kernel Density Estimations (KDE)")
593
+
594
+ fig_kde = px.line()
595
+ fig_kde.add_scatter(x=np.sort(df['interarrival_time']),
596
+ y=expon.pdf(np.sort(df['interarrival_time']), scale=df['interarrival_time'].mean()),
597
+ mode='lines', name='Interarrival KDE')
598
+ fig_kde.add_scatter(x=np.sort(df['service_time']),
599
+ y=expon.pdf(np.sort(df['service_time']), scale=df['service_time'].mean()),
600
+ mode='lines', name='Service KDE')
601
+ fig_kde.update_layout(title="Exponential fit (PDF overlaid)")
602
+ st.plotly_chart(fig_kde, use_container_width=True)
603
+
604
+ # πŸ”Ή Boxplots
605
+ st.subheader("πŸ“¦ Boxplots")
606
+
607
+ fig_box = px.box(df.melt(value_vars=['interarrival_time', 'service_time'], var_name='Type', value_name='Seconds'),
608
+ x='Type', y='Seconds', log_y=True,
609
+ title="Boxplot of interarrival vs service times (log Y)")
610
+ st.plotly_chart(fig_box, use_container_width=True)
611
+
612
+ # πŸ”Ή Utilization-performance curve
613
+ if metrics.get("rho", 2) < 1:
614
+ st.subheader("πŸ“ˆ Utilization-Performance Curve")
615
+ util_range = np.linspace(0.05, 0.99, 100)
616
+ L_curve = util_range / (1 - util_range)
617
+ fig_curve = px.line(x=util_range, y=L_curve,
618
+ labels={"x": "ρ", "y": "L"},
619
+ title="Avg number in system vs utilization (M/M/1)")
620
+ fig_curve.add_vline(x=metrics["rho"], line_dash="dash", annotation_text="current ρ")
621
+ st.plotly_chart(fig_curve, use_container_width=True)
622
+
623
+ # πŸ”Ή Export
624
+ if st.button("πŸ“₯ Export Metrics as CSV"):
625
+ if metrics.get("rho", 2) >= 1:
626
+ st.error("⚠️ Cannot export – system unstable.")
627
+ else:
628
+ csv_buf = io.StringIO()
629
+ pd.DataFrame([metrics]).to_csv(csv_buf, index=False)
630
+ st.download_button("Download CSV", csv_buf.getvalue(),
631
+ file_name="mm1_metrics.csv", mime="text/csv")