Spaces:
Sleeping
Sleeping
devjas1
commited on
Commit
·
503f867
1
Parent(s):
fbec946
FEAT(analyzer): Add theme-aware plotting and new spectrum rendering method; enhance visual diagnostics layout
Browse files- modules/analyzer.py +188 -79
- modules/ui_components.py +12 -11
modules/analyzer.py
CHANGED
|
@@ -7,9 +7,48 @@ import seaborn as sns
|
|
| 7 |
from sklearn.metrics import confusion_matrix
|
| 8 |
import matplotlib.pyplot as plt
|
| 9 |
from datetime import datetime
|
|
|
|
| 10 |
|
| 11 |
from config import LABEL_MAP # Assuming LABEL_MAP is correctly defined in config.py
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
class BatchAnalysis:
|
| 15 |
def __init__(self, df: pd.DataFrame):
|
|
@@ -66,93 +105,99 @@ class BatchAnalysis:
|
|
| 66 |
),
|
| 67 |
)
|
| 68 |
|
|
|
|
|
|
|
| 69 |
def render_visual_diagnostics(self):
|
| 70 |
"""
|
| 71 |
-
Renders the main diagnostic plots with improved aesthetics
|
|
|
|
| 72 |
"""
|
| 73 |
st.markdown("##### Visual Analysis")
|
| 74 |
if not self.has_ground_truth:
|
| 75 |
-
st.info(
|
| 76 |
-
"Visual analysis requires Ground Truth data, which is not available for this batch."
|
| 77 |
-
)
|
| 78 |
return
|
| 79 |
|
| 80 |
valid_gt_df = self.df.dropna(subset=["Ground Truth"])
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
fig, ax = plt.subplots(figsize=(5, 4), constrained_layout=True)
|
| 95 |
-
|
| 96 |
-
sns.heatmap(
|
| 97 |
-
cm,
|
| 98 |
-
annot=True,
|
| 99 |
-
fmt="g",
|
| 100 |
-
ax=ax,
|
| 101 |
-
cmap="Blues",
|
| 102 |
-
xticklabels=list(LABEL_MAP.values()),
|
| 103 |
-
yticklabels=list(LABEL_MAP.values()),
|
| 104 |
-
)
|
| 105 |
-
|
| 106 |
-
# Improve label readability and appearance
|
| 107 |
-
ax.set_ylabel("Actual Class", fontsize=12)
|
| 108 |
-
ax.set_xlabel("Predicted Class", fontsize=12)
|
| 109 |
-
ax.set_xticklabels(
|
| 110 |
-
ax.get_xticklabels(), rotation=45, ha="right"
|
| 111 |
-
) # Rotate labels to prevent overlap
|
| 112 |
-
ax.set_yticklabels(ax.get_yticklabels(), rotation=0)
|
| 113 |
-
|
| 114 |
-
# Use `use_container_width=True` to let Streamlit manage the plot's width
|
| 115 |
-
st.pyplot(fig, use_container_width=True)
|
| 116 |
-
|
| 117 |
-
# --- Chart 2: Confidence vs. Correctness Box Plot (Aesthetically Improved) ---
|
| 118 |
-
with viz_cols[1]:
|
| 119 |
-
st.markdown("**Confidence Analysis**")
|
| 120 |
-
valid_gt_df["Result"] = np.where(
|
| 121 |
-
valid_gt_df["Prediction"] == valid_gt_df["Ground Truth"],
|
| 122 |
-
"Correct",
|
| 123 |
-
"Incorrect",
|
| 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 |
def _set_cm_filter(
|
| 157 |
self,
|
| 158 |
actual_idx: int,
|
|
@@ -294,16 +339,80 @@ class BatchAnalysis:
|
|
| 294 |
st.session_state.selected_spectrum_file = None
|
| 295 |
# --- END ROBUST HANDLING ---
|
| 296 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
def render(self):
|
| 298 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 299 |
if self.df.empty:
|
| 300 |
st.info(
|
| 301 |
"The results table is empty. Please run an analysis on the 'Upload and Run' page."
|
| 302 |
)
|
| 303 |
return
|
| 304 |
|
|
|
|
| 305 |
self.render_kpis()
|
| 306 |
st.divider()
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from sklearn.metrics import confusion_matrix
|
| 8 |
import matplotlib.pyplot as plt
|
| 9 |
from datetime import datetime
|
| 10 |
+
from contextlib import contextmanager # Correctly imported for use with @contextmanager
|
| 11 |
|
| 12 |
from config import LABEL_MAP # Assuming LABEL_MAP is correctly defined in config.py
|
| 13 |
|
| 14 |
+
# --- ADD THESE IMPORTS AT THE TOP OF THE FILE ---
|
| 15 |
+
from utils.results_manager import ResultsManager
|
| 16 |
+
from modules.ui_components import create_spectrum_plot
|
| 17 |
+
import hashlib
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# --- NEW HELPER FUNCTION for theme-aware plots ---
|
| 21 |
+
@contextmanager
|
| 22 |
+
def theme_aware_plot():
|
| 23 |
+
"""A context manager to make Matplotlib plots respect Streamlit's theme."""
|
| 24 |
+
# Get the current theme from Streamlit's config with error handling
|
| 25 |
+
try:
|
| 26 |
+
theme_opts = st.get_option("theme") or {}
|
| 27 |
+
except RuntimeError:
|
| 28 |
+
# Fallback to empty dict if theme config is not available
|
| 29 |
+
theme_opts = {}
|
| 30 |
+
|
| 31 |
+
text_color = theme_opts.get("textColor", "#000000")
|
| 32 |
+
bg_color = theme_opts.get("backgroundColor", "#FFFFFF")
|
| 33 |
+
|
| 34 |
+
# Set Matplotlib's rcParams to match the theme
|
| 35 |
+
with plt.rc_context(
|
| 36 |
+
{
|
| 37 |
+
"figure.facecolor": bg_color,
|
| 38 |
+
"axes.facecolor": bg_color,
|
| 39 |
+
"text.color": text_color,
|
| 40 |
+
"axes.labelcolor": text_color,
|
| 41 |
+
"xtick.color": text_color,
|
| 42 |
+
"ytick.color": text_color,
|
| 43 |
+
"grid.color": text_color,
|
| 44 |
+
"axes.edgecolor": text_color,
|
| 45 |
+
}
|
| 46 |
+
):
|
| 47 |
+
yield
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# --- END HELPER FUNCTION ---
|
| 51 |
+
|
| 52 |
|
| 53 |
class BatchAnalysis:
|
| 54 |
def __init__(self, df: pd.DataFrame):
|
|
|
|
| 105 |
),
|
| 106 |
)
|
| 107 |
|
| 108 |
+
# In modules/analyzer.py
|
| 109 |
+
|
| 110 |
def render_visual_diagnostics(self):
|
| 111 |
"""
|
| 112 |
+
Renders the main diagnostic plots with improved aesthetics, layout,
|
| 113 |
+
and automatic theme adaptation.
|
| 114 |
"""
|
| 115 |
st.markdown("##### Visual Analysis")
|
| 116 |
if not self.has_ground_truth:
|
| 117 |
+
st.info("Visual analysis requires Ground Truth data for this batch.")
|
|
|
|
|
|
|
| 118 |
return
|
| 119 |
|
| 120 |
valid_gt_df = self.df.dropna(subset=["Ground Truth"])
|
| 121 |
|
| 122 |
+
# Use a single row of columns for the two main plots
|
| 123 |
+
plot_col1, plot_col2 = st.columns(2)
|
| 124 |
+
|
| 125 |
+
# --- Chart 1: Confusion Matrix ---
|
| 126 |
+
with plot_col1: # Content for the first column
|
| 127 |
+
with st.container(border=True): # Group plot and buttons visually
|
| 128 |
+
st.markdown("**Confusion Matrix**")
|
| 129 |
+
cm = confusion_matrix(
|
| 130 |
+
valid_gt_df["Ground Truth"],
|
| 131 |
+
valid_gt_df["Prediction"],
|
| 132 |
+
labels=list(LABEL_MAP.keys()),
|
| 133 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
with theme_aware_plot(): # Apply theme-aware styling
|
| 136 |
+
fig, ax = plt.subplots(figsize=(5, 4), constrained_layout=True)
|
| 137 |
+
sns.heatmap(
|
| 138 |
+
cm,
|
| 139 |
+
annot=True,
|
| 140 |
+
fmt="g",
|
| 141 |
+
ax=ax,
|
| 142 |
+
cmap="Blues",
|
| 143 |
+
xticklabels=list(LABEL_MAP.values()),
|
| 144 |
+
yticklabels=list(LABEL_MAP.values()),
|
| 145 |
+
)
|
| 146 |
+
ax.set_ylabel("Actual Class", fontsize=12)
|
| 147 |
+
ax.set_xlabel("Predicted Class", fontsize=12)
|
| 148 |
+
ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha="right")
|
| 149 |
+
ax.set_yticklabels(ax.get_yticklabels(), rotation=0)
|
| 150 |
+
st.pyplot(fig, use_container_width=True) # Render the plot
|
| 151 |
+
|
| 152 |
+
st.caption("Click a cell below to filter the data grid:")
|
| 153 |
+
|
| 154 |
+
# Render CM filter buttons directly below the plot in the same column
|
| 155 |
+
cm_labels = list(LABEL_MAP.values())
|
| 156 |
+
for i, actual_label in enumerate(cm_labels):
|
| 157 |
+
btn_cols_row = st.columns(
|
| 158 |
+
len(cm_labels)
|
| 159 |
+
) # Create a row of columns for buttons
|
| 160 |
+
for j, predicted_label in enumerate(cm_labels):
|
| 161 |
+
cell_value = cm[i, j]
|
| 162 |
+
btn_cols_row[j].button( # Button for each cell
|
| 163 |
+
f"Actual: {actual_label}\nPred: {predicted_label} ({cell_value})",
|
| 164 |
+
key=f"cm_cell_{i}_{j}",
|
| 165 |
+
on_click=self._set_cm_filter,
|
| 166 |
+
args=(i, j, actual_label, predicted_label),
|
| 167 |
+
use_container_width=True,
|
| 168 |
+
)
|
| 169 |
+
# Clear filter button for CM
|
| 170 |
+
if st.session_state.get("cm_filter_active", False):
|
| 171 |
+
st.button(
|
| 172 |
+
"Clear Matrix Filter",
|
| 173 |
+
on_click=self._clear_cm_filter,
|
| 174 |
+
key="clear_cm_filter_btn_below",
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
# --- Chart 2: Confidence vs. Correctness Box Plot ---
|
| 178 |
+
with plot_col2: # Content for the second column
|
| 179 |
+
with st.container(border=True): # Group plot visually
|
| 180 |
+
st.markdown("**Confidence Analysis**")
|
| 181 |
+
valid_gt_df["Result"] = np.where(
|
| 182 |
+
valid_gt_df["Prediction"] == valid_gt_df["Ground Truth"],
|
| 183 |
+
"Correct",
|
| 184 |
+
"Incorrect",
|
| 185 |
)
|
| 186 |
|
| 187 |
+
with theme_aware_plot(): # Apply theme-aware styling
|
| 188 |
+
fig, ax = plt.subplots(figsize=(5, 4), constrained_layout=True)
|
| 189 |
+
sns.boxplot(
|
| 190 |
+
x="Result",
|
| 191 |
+
y="Confidence",
|
| 192 |
+
data=valid_gt_df,
|
| 193 |
+
ax=ax,
|
| 194 |
+
palette={"Correct": "#64C764", "Incorrect": "#E57373"},
|
| 195 |
+
)
|
| 196 |
+
ax.set_ylabel("Model Confidence", fontsize=12)
|
| 197 |
+
ax.set_xlabel("Prediction Result", fontsize=12)
|
| 198 |
+
st.pyplot(fig, use_container_width=True)
|
| 199 |
+
st.divider() # Divider after the entire visual section
|
| 200 |
+
|
| 201 |
def _set_cm_filter(
|
| 202 |
self,
|
| 203 |
actual_idx: int,
|
|
|
|
| 339 |
st.session_state.selected_spectrum_file = None
|
| 340 |
# --- END ROBUST HANDLING ---
|
| 341 |
|
| 342 |
+
# --- ADD THIS ENTIRE NEW METHOD ---
|
| 343 |
+
def render_selected_spectrum(self):
|
| 344 |
+
"""
|
| 345 |
+
Renders an expander with the spectrum plot for the currently selected file.
|
| 346 |
+
This is called after the data grid.
|
| 347 |
+
"""
|
| 348 |
+
selected_file = st.session_state.get("selected_spectrum_file")
|
| 349 |
+
|
| 350 |
+
# Only render if a file has been selected in the current session
|
| 351 |
+
if selected_file:
|
| 352 |
+
with st.expander(
|
| 353 |
+
f"🔬 View Spectrum for: **{selected_file}**", expanded=True
|
| 354 |
+
):
|
| 355 |
+
# Retrieve the full, detailed record for the selected file
|
| 356 |
+
spectrum_data = ResultsManager.get_spectrum_data_for_file(selected_file)
|
| 357 |
+
|
| 358 |
+
# Check if the detailed data was successfully retrieved and contains all necessary arrays
|
| 359 |
+
if spectrum_data and all(
|
| 360 |
+
spectrum_data.get(k) is not None
|
| 361 |
+
for k in ["x_raw", "y_raw", "x_resampled", "y_resampled"]
|
| 362 |
+
):
|
| 363 |
+
# Generate a unique cache key for the plot to avoid re-generating it unnecessarily
|
| 364 |
+
cache_key = hashlib.md5(
|
| 365 |
+
(
|
| 366 |
+
f"{spectrum_data['x_raw'].tobytes()}"
|
| 367 |
+
f"{spectrum_data['y_raw'].tobytes()}"
|
| 368 |
+
f"{spectrum_data['x_resampled'].tobytes()}"
|
| 369 |
+
f"{spectrum_data['y_resampled'].tobytes()}"
|
| 370 |
+
).encode()
|
| 371 |
+
).hexdigest()
|
| 372 |
+
|
| 373 |
+
# Call the plotting function from ui_components
|
| 374 |
+
plot_image = create_spectrum_plot(
|
| 375 |
+
spectrum_data["x_raw"],
|
| 376 |
+
spectrum_data["y_raw"],
|
| 377 |
+
spectrum_data["x_resampled"],
|
| 378 |
+
spectrum_data["y_resampled"],
|
| 379 |
+
_cache_key=cache_key,
|
| 380 |
+
)
|
| 381 |
+
st.image(
|
| 382 |
+
plot_image,
|
| 383 |
+
caption=f"Raw vs. Resampled Spectrum for {selected_file}",
|
| 384 |
+
use_container_width=True,
|
| 385 |
+
)
|
| 386 |
+
else:
|
| 387 |
+
st.warning(
|
| 388 |
+
f"Could not retrieve spectrum data for '{selected_file}'. The data might not have been stored during the initial run."
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
# --- END NEW METHOD ---
|
| 392 |
+
|
| 393 |
def render(self):
|
| 394 |
+
"""
|
| 395 |
+
The main public method to render the entire dashboard using a more
|
| 396 |
+
organized and streamlined tab-based layout.
|
| 397 |
+
"""
|
| 398 |
if self.df.empty:
|
| 399 |
st.info(
|
| 400 |
"The results table is empty. Please run an analysis on the 'Upload and Run' page."
|
| 401 |
)
|
| 402 |
return
|
| 403 |
|
| 404 |
+
# --- Tier 1: KPIs (Always visible at the top) ---
|
| 405 |
self.render_kpis()
|
| 406 |
st.divider()
|
| 407 |
+
|
| 408 |
+
# --- Tier 2: Tabbed Interface for Deeper Analysis ---
|
| 409 |
+
tab1, tab2 = st.tabs(["📊 Visual Diagnostics", "🗂️ Results Explorer"])
|
| 410 |
+
|
| 411 |
+
with tab1:
|
| 412 |
+
# The visual diagnostics (Confusion Matrix, etc.) go here.
|
| 413 |
+
self.render_visual_diagnostics()
|
| 414 |
+
|
| 415 |
+
with tab2:
|
| 416 |
+
# The interactive grid AND the spectrum viewer it controls go here.
|
| 417 |
+
self.render_interactive_grid()
|
| 418 |
+
self.render_selected_spectrum()
|
modules/ui_components.py
CHANGED
|
@@ -180,22 +180,22 @@ def render_sidebar():
|
|
| 180 |
with st.expander("About This App", icon=":material/info:", expanded=False):
|
| 181 |
st.markdown(
|
| 182 |
"""
|
| 183 |
-
AI-Driven Polymer Aging Prediction and Classification
|
| 184 |
|
| 185 |
-
**Purpose**: Classify polymer degradation using AI
|
| 186 |
-
**Input**: Raman spectroscopy
|
| 187 |
-
**Models**: CNN architectures for binary classification
|
| 188 |
-
**Next**: More trained CNNs in evaluation pipeline
|
| 189 |
|
| 190 |
|
| 191 |
-
**Contributors
|
| 192 |
-
Dr. Sanmukh Kuppannagari (Mentor)
|
| 193 |
-
Dr. Metin Karailyan (Mentor)
|
| 194 |
-
Jaser Hasan (Author)
|
| 195 |
|
| 196 |
|
| 197 |
-
**Links
|
| 198 |
-
[
|
| 199 |
[GitHub Repository](https://github.com/KLab-AI3/ml-polymer-recycling)
|
| 200 |
|
| 201 |
|
|
@@ -203,6 +203,7 @@ def render_sidebar():
|
|
| 203 |
Neo et al., 2023, *Resour. Conserv. Recycl.*, 188, 106718.
|
| 204 |
[https://doi.org/10.1016/j.resconrec.2022.106718](https://doi.org/10.1016/j.resconrec.2022.106718)
|
| 205 |
""",
|
|
|
|
| 206 |
)
|
| 207 |
|
| 208 |
|
|
|
|
| 180 |
with st.expander("About This App", icon=":material/info:", expanded=False):
|
| 181 |
st.markdown(
|
| 182 |
"""
|
| 183 |
+
**AI-Driven Polymer Aging Prediction and Classification**
|
| 184 |
|
| 185 |
+
**Purpose**: Classify polymer degradation using AI<br>
|
| 186 |
+
**Input**: Raman spectroscopy .txt files<br>
|
| 187 |
+
**Models**: CNN architectures for binary classification<br>
|
| 188 |
+
**Next**: More trained CNNs in evaluation pipeline<br>
|
| 189 |
|
| 190 |
|
| 191 |
+
**Contributors**<br>
|
| 192 |
+
- Dr. Sanmukh Kuppannagari (Mentor)<br>
|
| 193 |
+
- Dr. Metin Karailyan (Mentor)<br>
|
| 194 |
+
- Jaser Hasan (Author)<br>
|
| 195 |
|
| 196 |
|
| 197 |
+
**Links**<br>
|
| 198 |
+
[HF Space](https://huggingface.co/spaces/dev-jas/polymer-aging-ml)<br>
|
| 199 |
[GitHub Repository](https://github.com/KLab-AI3/ml-polymer-recycling)
|
| 200 |
|
| 201 |
|
|
|
|
| 203 |
Neo et al., 2023, *Resour. Conserv. Recycl.*, 188, 106718.
|
| 204 |
[https://doi.org/10.1016/j.resconrec.2022.106718](https://doi.org/10.1016/j.resconrec.2022.106718)
|
| 205 |
""",
|
| 206 |
+
unsafe_allow_html=True,
|
| 207 |
)
|
| 208 |
|
| 209 |
|