Upload 2 files
Browse files- Dockerfile +20 -0
- 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")
|