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
Files changed (2) hide show
  1. modules/analyzer.py +188 -79
  2. 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 and layout.
 
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
- viz_cols = st.columns(2)
83
-
84
- # --- Chart 1: Confusion Matrix (Aesthetically Improved) ---
85
- with viz_cols[0]:
86
- st.markdown("**Confusion Matrix**")
87
- cm = confusion_matrix(
88
- valid_gt_df["Ground Truth"],
89
- valid_gt_df["Prediction"],
90
- labels=list(LABEL_MAP.keys()),
91
- )
92
-
93
- # Use Matplotlib's constrained_layout for better sizing
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
- fig, ax = plt.subplots(figsize=(5, 4), constrained_layout=True)
127
-
128
- sns.boxplot(
129
- x="Result",
130
- y="Confidence",
131
- data=valid_gt_df,
132
- ax=ax,
133
- palette={"Correct": "#64C764", "Incorrect": "#E57373"},
134
- ) # Use softer colors
135
-
136
- ax.set_ylabel("Model Confidence", fontsize=12)
137
- ax.set_xlabel("Prediction Result", fontsize=12)
138
-
139
- st.pyplot(fig, use_container_width=True)
140
-
141
- # ... (The interactive button grid for the confusion matrix remains the same) ...
142
- st.markdown("Click on a cell below to filter the results grid:")
143
- cm_labels = list(LABEL_MAP.values())
144
- for i, actual_label in enumerate(cm_labels):
145
- cols = st.columns(len(cm_labels))
146
- for j, predicted_label in enumerate(cm_labels):
147
- cell_value = cm[i, j]
148
- cols[j].button(
149
- f"Actual: {actual_label}\nPred: {predicted_label} ({cell_value})",
150
- key=f"cm_cell_{i}_{j}",
151
- on_click=self._set_cm_filter,
152
- args=(i, j, actual_label, predicted_label),
153
- use_container_width=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- """The main public method to render the entire dashboard."""
 
 
 
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
- self.render_visual_diagnostics()
308
- st.divider()
309
- self.render_interactive_grid()
 
 
 
 
 
 
 
 
 
 
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 `.txt` files
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
- [Live HF Space](https://huggingface.co/spaces/dev-jas/polymer-aging-ml)
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