Spaces:
Running
Running
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 |
|