pfas-sbead-optimization / utils /visualizations.py
shrut27's picture
Upload folder using huggingface_hub
bcb2d6c verified
"""Plotly visualization helpers for the PFAS-SBEAD dashboard."""
from __future__ import annotations
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
COLOR_SCHEME = px.colors.qualitative.Set2
TEMPLATE = "plotly_white"
def degradation_scatter(df: pd.DataFrame) -> go.Figure:
"""Scatter: PFAS degradation vs voltage, colored by current density."""
fig = px.scatter(
df,
x="voltage_V",
y="PFAS_degradation_pct",
color="current_density_A_m2",
size="HRT_days",
hover_data=["OLR_kg_m3_d", "pH", "AI_score"],
title="PFAS Degradation vs Applied Voltage",
labels={
"voltage_V": "Voltage (V)",
"PFAS_degradation_pct": "PFAS Degradation (%)",
"current_density_A_m2": "Current Density (A/m²)",
"HRT_days": "HRT (days)",
},
color_continuous_scale="Viridis",
template=TEMPLATE,
)
fig.update_layout(height=450)
return fig
def ai_score_distribution(df: pd.DataFrame) -> go.Figure:
"""Histogram of AI optimization scores."""
fig = px.histogram(
df,
x="AI_score",
nbins=25,
title="AI Optimization Score Distribution",
labels={"AI_score": "AI Score"},
color_discrete_sequence=[COLOR_SCHEME[0]],
template=TEMPLATE,
)
fig.update_layout(height=350)
return fig
def mass_balance_sunburst(mb_df: pd.DataFrame) -> go.Figure:
"""Average mass balance breakdown as a pie chart."""
avg = mb_df[
["remaining_in_water_ug_L", "adsorbed_sludge_ug_L",
"adsorbed_electrode_ug_L", "short_chain_products_ug_L", "mineralized_PFAS_ug_L"]
].mean()
labels = ["Remaining in Water", "Adsorbed on Sludge", "Adsorbed on Electrode",
"Short-Chain Products", "Mineralized (Degraded)"]
fig = go.Figure(data=[go.Pie(
labels=labels,
values=avg.values,
hole=0.4,
marker_colors=COLOR_SCHEME[:5],
)])
fig.update_layout(
title="Average PFAS Mass Balance Distribution",
template=TEMPLATE,
height=400,
)
return fig
def feature_importance_bar(imp_df: pd.DataFrame) -> go.Figure:
"""Horizontal bar chart of feature importances."""
fig = px.bar(
imp_df.head(10),
x="importance",
y="feature",
orientation="h",
title="Top Feature Importances (SHAP-proxy)",
labels={"importance": "Importance", "feature": ""},
color="importance",
color_continuous_scale="Blues",
template=TEMPLATE,
)
fig.update_layout(height=400, showlegend=False)
return fig
def degradation_heatmap(df: pd.DataFrame) -> go.Figure:
"""Heatmap: voltage vs OLR bins, showing mean degradation."""
df_copy = df.copy()
df_copy["OLR_bin"] = pd.cut(df_copy["OLR_kg_m3_d"], bins=6)
df_copy["voltage_bin"] = pd.cut(df_copy["voltage_V"], bins=6)
pivot = df_copy.pivot_table(
values="PFAS_degradation_pct",
index="voltage_bin",
columns="OLR_bin",
aggfunc="mean",
)
fig = px.imshow(
pivot.values,
x=[str(c) for c in pivot.columns],
y=[str(i) for i in pivot.index],
color_continuous_scale="YlOrRd",
title="PFAS Degradation Heatmap: Voltage × OLR",
labels={"x": "OLR Bin (kg/m³/d)", "y": "Voltage Bin (V)", "color": "Degradation (%)"},
template=TEMPLATE,
)
fig.update_layout(height=420)
return fig
def dual_axis_performance(df: pd.DataFrame) -> go.Figure:
"""Dual-axis: degradation and fluoride release vs experiment."""
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(
go.Scatter(
x=df["experiment_id"],
y=df["PFAS_degradation_pct"],
mode="lines+markers",
name="PFAS Degradation (%)",
marker=dict(size=5, color=COLOR_SCHEME[0]),
),
secondary_y=False,
)
fig.add_trace(
go.Scatter(
x=df["experiment_id"],
y=df["fluoride_release_mg_L"],
mode="lines+markers",
name="Fluoride Release (mg/L)",
marker=dict(size=5, color=COLOR_SCHEME[1]),
),
secondary_y=True,
)
fig.update_layout(
title="Degradation & Fluoride Release Across Experiments",
template=TEMPLATE,
height=400,
legend=dict(orientation="h", yanchor="bottom", y=1.02),
)
fig.update_yaxes(title_text="PFAS Degradation (%)", secondary_y=False)
fig.update_yaxes(title_text="Fluoride Release (mg/L)", secondary_y=True)
return fig
def stability_radar(df: pd.DataFrame) -> go.Figure:
"""Radar chart of average stability indicators."""
cols = ["pH_drop", "current_instability_index"]
vfa_norm = df["VFA_accumulation_mg_L"] / df["VFA_accumulation_mg_L"].max()
orp_norm = df["ORP_drift_mV"].abs() / df["ORP_drift_mV"].abs().max()
values = [
df["pH_drop"].mean() / 1.5,
vfa_norm.mean(),
orp_norm.mean(),
df["current_instability_index"].mean() / 0.5,
]
categories = ["pH Drop", "VFA Accumulation", "ORP Drift", "Current Instability"]
values.append(values[0])
categories.append(categories[0])
fig = go.Figure(data=go.Scatterpolar(
r=values,
theta=categories,
fill="toself",
fillcolor="rgba(255, 99, 71, 0.2)",
line_color="tomato",
))
fig.update_layout(
polar=dict(radialaxis=dict(visible=True, range=[0, 1])),
title="Reactor Stability Indicators (Normalized)",
template=TEMPLATE,
height=400,
)
return fig
def sensitivity_bar(sens_df: pd.DataFrame) -> go.Figure:
"""Bar chart of sensitivity analysis."""
fig = px.bar(
sens_df,
x="feature",
y="correlation_with_AI_score",
color="correlation_with_AI_score",
color_continuous_scale="RdYlGn",
title="Sensitivity Analysis: Feature Correlation with AI Score",
labels={"feature": "", "correlation_with_AI_score": "Correlation"},
template=TEMPLATE,
)
fig.update_layout(height=400, xaxis_tickangle=-45)
return fig
def energy_vs_degradation(df: pd.DataFrame) -> go.Figure:
"""Scatter: energy input vs degradation with instability flag."""
fig = px.scatter(
df,
x="energy_input_kWh_d",
y="PFAS_degradation_pct",
color="instability_flag",
symbol="instability_flag",
title="Energy Input vs PFAS Degradation (Instability Highlighted)",
labels={
"energy_input_kWh_d": "Energy Input (kWh/d)",
"PFAS_degradation_pct": "PFAS Degradation (%)",
"instability_flag": "Instability",
},
color_discrete_map={0: COLOR_SCHEME[0], 1: "red"},
template=TEMPLATE,
)
fig.update_layout(height=400)
return fig
def optimization_pareto(df: pd.DataFrame) -> go.Figure:
"""Pareto front: degradation vs energy showing trade-off."""
fig = px.scatter(
df,
x="energy_input_kWh_d",
y="PFAS_degradation_pct",
color="AI_score",
size="fluoride_release_mg_L",
hover_data=["voltage_V", "HRT_days", "OLR_kg_m3_d"],
title="Optimization Landscape: Degradation vs Energy Trade-off",
labels={
"energy_input_kWh_d": "Energy Input (kWh/d)",
"PFAS_degradation_pct": "PFAS Degradation (%)",
"AI_score": "AI Score",
},
color_continuous_scale="Plasma",
template=TEMPLATE,
)
fig.update_layout(height=450)
return fig