Testing / src /streamlit_app.py
wayne-chi's picture
Update src/streamlit_app.py
7efa5c9 verified
import streamlit as st
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import plotly.express as px
import numpy as np
import plotly.graph_objects as go
import shutil
import os
# from blend_logic import run_dummy_prediction
##---- fucntions ------
import pandas as pd
import streamlit as st
# Load fuel data from CSV (create this file if it doesn't exist)
SRC_CSV_PATH = os.path.join(os.path.dirname(__file__), "fuel_properties.csv")
# Writable path (e.g., /tmp or home dir)
FUEL_CSV_PATH = os.path.join("/tmp", "fuel_properties.csv")
# If not already copied, copy it over
if not os.path.exists(FUEL_CSV_PATH):
shutil.copy(SRC_CSV_PATH, FUEL_CSV_PATH)
def load_fuel_data():
"""Load fuel data from CSV or create default if not exists"""
try:
df = pd.read_csv(FUEL_CSV_PATH, index_col=0)
return df.to_dict('index')
except FileNotFoundError:
# Create default fuel properties if file doesn't exist
default_fuels = {
"Gasoline": {f"Property{i+1}": round(0.7 + (i*0.02), 1) for i in range(10)},
"Diesel": {f"Property{i+1}": round(0.8 + (i*0.02), 1) for i in range(10)},
"Ethanol": {f"Property{i+1}": round(0.75 + (i*0.02), 1) for i in range(10)},
"Biodiesel": {f"Property{i+1}": round(0.85 + (i*0.02), 1) for i in range(10)},
"Jet Fuel": {f"Property{i+1}": round(0.78 + (i*0.02), 1) for i in range(10)}
}
pd.DataFrame(default_fuels).T.to_csv(FUEL_CSV_PATH)
return default_fuels
# Initialize or load fuel data
if 'FUEL_PROPERTIES' not in st.session_state:
st.session_state.FUEL_PROPERTIES = load_fuel_data()
def save_fuel_data():
"""Save current fuel data to CSV"""
pd.DataFrame(st.session_state.FUEL_PROPERTIES).T.to_csv(FUEL_CSV_PATH)
# FUEL_PROPERTIES = st.session_state.FUEL_PROPERTIES
# ---------------------- Page Config ----------------------
st.set_page_config(
layout="wide",
page_title="Eagle Blend Optimizer",
page_icon="πŸ¦…",
initial_sidebar_state="expanded"
)
# ---------------------- Custom Styling ---------------------- ##e0e0e0;
st.markdown("""
<style>
.block-container {
padding-top: 1rem;
}
/* Main app background */
.stApp {
background-color: #f8f5f0;
overflow: visible;
padding-top: 0
}
/* Remove unnecessary space at the top */
/* Remove any fixed headers */
.stApp > header {
position: static !important;
}
/* Header styling */
.header {
background: linear-gradient(135deg, #654321 0%, #8B4513 100%);
color: white;
padding: 2rem 1rem;
margin-bottom: 2rem;
border-radius: 0 0 15px 15px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Metric card styling */
.metric-card {
background: #ffffff; /* Pure white cards for contrast */
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
height: 100%;
transition: all 0.3s ease;
border: 1px solid #CFB53B;
}
.metric-card:hover {
transform: translateY(-3px);
background: #FFF8E1; /* Very light blue tint on hover */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
border-color: #8B4513;
}
/* Metric value styling */
.metric-value {
color: #8B4513 !important; /* Deep, vibrant blue */
font-weight: 700;
font-size: 1.8rem;
text-shadow: 0 1px 2px rgba(0, 82, 204, 0.1);
}
/* Metric label styling */
.metric-label {
color: #654321; /* Navy blue-gray */
font-weight: 600;
letter-spacing: 0.5px;
}
/* Metric delta styling */
.metric-delta {
color: #A67C52; /* Medium blue-gray */
font-size: 0.9rem;
font-weight: 500;
}
/* Tab styling */
/* Main tab container */
.stTabs [data-baseweb="tab-list"] {
display: flex;
justify-content: center;
gap: 6px;
padding: 8px;
margin: 0 auto;
width: 95% !important;
}
/* Individual tabs */
.stTabs [data-baseweb="tab"] {
flex: 1; /* Equal width distribution */
min-width: 0; /* Allows flex to work */
height: 60px; /* Fixed height or use aspect ratio */
padding: 0 12px;
margin: 0;
font-weight: 600;
font-size: 1rem;
color: #654321;
background: #FFF8E1;
border: 2px solid #CFB53B;
border-radius: 12px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
/* Hover state */
.stTabs [data-baseweb="tab"]:hover {
background: #FFE8A1;
transform: translateY(-2px);
}
/* Active tab */
.stTabs [aria-selected="true"] {
background: #654321;
color: #FFD700 !important;
border-color: #8B4513;
font-size: 1.05rem;
}
/* Icon sizing */
.stTabs [data-baseweb="tab"] svg {
width: 24px !important;
height: 24px !important;
margin-right: 8px !important;
}
/* Button styling */
.stButton>button {
background-color: #654321;
color: #FFD700 !important;
border-radius: 8px;
padding: 0.5rem 1rem;
transition: all 0.3s ease;
}
.stButton>button:hover {
background-color: #8B4513;
color: white;
}
/* Dataframe styling */
.table-container {
display: flex;
justify-content: center;
margin-top: 30px;
}
.table-inner {
width: 50%;
}
@media only screen and (max-width: 768px) {
.table-inner {
width: 90%; /* For mobile */
}
}
.stDataFrame {
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
background-color:white !important;
border: #CFB53B !important;
}
/* Section headers */
.st-emotion-cache-16txtl3 {
padding-top: 1rem;
}
/* Custom hr style */
.custom-divider {
border: 0;
height: 1px;
background: linear-gradient(90deg, transparent, #dee2e6, transparent);
margin: 2rem 0;
}
/* Consistent chart styling */
.stPlotlyChart {
border-radius: 10px;
background: white;
padding: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 25px;
}
/* Match number inputs */
# .stNumberInput > div {
# padding: 0.25rem 0.5rem !important;
# }
#/* Better select widget alignment */
# .stSelectbox > div {
# margin-bottom: -15px;
# }
.custom-uploader > label div[data-testid="stFileUploadDropzone"] {
border: 2px solid #4CAF50;
background-color: #4CAF50;
color: white;
padding: 0.6em 1em;
border-radius: 0.5em;
text-align: center;
cursor: pointer;
}
.custom-uploader > label div[data-testid="stFileUploadDropzone"]:hover {
background-color: #45a049;
}
/* Color scale adjustments */
.plotly .colorbar {
padding: 10px !important;
color: #654321 !important;
}
</style>
""", unsafe_allow_html=True)
# ---------------------- App Header ----------------------
st.markdown("""
<div class="header">
<h1 style='text-align: center; margin-bottom: 0.5rem;'>πŸ¦… Eagle Blend Optimizer</h1>
<h4 style='text-align: center; font-weight: 400; margin-top: 0;'>
AI-Powered Fuel Blend Property Prediction & Optimization
</h4>
</div>
""", unsafe_allow_html=True)
#------ universal variables
# ---------------------- Tabs ----------------------
tabs = st.tabs([
"πŸ“Š Dashboard",
"πŸŽ›οΈ Blend Designer",
"πŸ“€ Nothing For Now",
"βš™οΈ Optimization Engine",
"πŸ“š Fuel Registry",
"🧠 Model Insights"
])
# ---------------------- Dashboard Tab ----------------------
with tabs[0]:
st.subheader("Performance Metrics")
col1, col2, col3, col4 = st.columns(4)
with col1:
st.markdown("""
<div class="metric-card">
<div class="metric-label">Model Accuracy</div>
<div class="metric-value">94.7%</div>
<div class="metric-delta">RΒ² Score</div>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown("""
<div class="metric-card">
<div class="metric-label">Predictions Made</div>
<div class="metric-value">12,847</div>
<div class="metric-delta">Today</div>
</div>
""", unsafe_allow_html=True)
with col3:
st.markdown("""
<div class="metric-card">
<div class="metric-label">Optimizations</div>
<div class="metric-value">156</div>
<div class="metric-delta">This Week</div>
</div>
""", unsafe_allow_html=True)
with col4:
st.markdown("""
<div class="metric-card">
<div class="metric-label">Cost Savings</div>
<div class="metric-value">$2.4M</div>
<div class="metric-delta">Estimated Annual</div>
</div>
""", unsafe_allow_html=True)
st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
st.subheader("Current Blend Properties")
blend_props = {
"Property 1": 0.847,
"Property 2": 0.623,
"Property 3": 0.734,
"Property 4": 0.912,
"Property 5": 0.456,
"Property 6": -1.234,
}
# Enhanced dataframe display
df = pd.DataFrame(blend_props.items(), columns=["Property", "Value"])
# st.dataframe(
# df.style
# .background_gradient(cmap="YlOrBr", subset=["Value"])
# .format({"Value": "{:.3f}"}),
# use_container_width=True
# )
st.markdown('<div class="table-container"><div class="table-inner">', unsafe_allow_html=True)
st.dataframe(df, use_container_width=True)
st.markdown('</div></div>', unsafe_allow_html=True)
with tabs[1]:
col_header = st.columns([0.8, 0.2])
with col_header[0]:
st.subheader("πŸŽ›οΈ Blend Designer")
with col_header[1]:
batch_blend = st.checkbox("Batch Blend Mode", value=False,
help="Switch between manual input and predefined fuel selection",
key="batch_blend_mode")
# Initialize session state
if 'show_visualization' not in st.session_state:
st.session_state.show_visualization = False
if 'blended_value' not in st.session_state:
st.session_state.blended_value = None
if 'selected_property' not in st.session_state:
st.session_state.selected_property = "Property1"
# Batch mode file upload
if batch_blend:
st.subheader("πŸ“€ Batch Processing")
uploaded_file = st.file_uploader("Upload CSV File", type=["csv"], key="Batch_upload")
weights = [0.1, 0.2, 0.25, 0.15, 0.3] # Default weights for batch mode
if not uploaded_file:
st.warning("Please upload a CSV file for batch processing")
data_input = None
else:
try:
data_input = pd.read_csv(uploaded_file)
st.success("File uploaded successfully")
st.dataframe(data_input.head())
except Exception as e:
st.error(f"Error reading file: {str(e)}")
data_input = None
else:
# Regular mode
data_input = None
weights, props = [], []
col1, col2 = st.columns(2)
with col1:
st.markdown("##### βš–οΈ Component Weights")
for i in range(5):
weight = st.number_input(
f"Weight for Component {i+1}",
min_value=0.0,
max_value=1.0,
value=0.2,
step=0.01,
key=f"w_{i}"
)
weights.append(weight)
with col2:
st.markdown("##### Fuel Selection")
for i in range(5):
fuel = st.selectbox(
f"Component {i+1} Fuel Type",
options=list(st.session_state.FUEL_PROPERTIES.keys()),
key=f"fuel_{i}"
)
props.append(st.session_state.FUEL_PROPERTIES[fuel])
if st.button("βš™οΈ Predict Blended Property", key="predict_btn"):
if batch_blend:
if data_input is None:
st.error("⚠️ Please upload a valid CSV file first!")
st.session_state.show_visualization = False
else:
st.session_state.show_visualization = True
else:
if abs(sum(weights) - 1.0) > 0.01:
st.warning("⚠️ The total of weights must be **1.0**.")
st.session_state.show_visualization = False
else:
st.session_state.show_visualization = True
if st.session_state.show_visualization:
# Show calculation details
st.subheader("Blend Components Data")
if not batch_blend:
weights_data = {f"Component{i+1}_fraction": weights[i] for i in range(len(weights))}
props_data = {f"Component{i+1}_{j}": props[i][j] for j in props[i].keys() for i in range(len(props))}
combined = {**weights_data, **props_data}
data_input = pd.DataFrame([combined])
st.write("Properties:", data_input)
# Show visualization only if prediction was made
if st.session_state.show_visualization:
if not batch_blend:
st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
st.subheader("Blend Visualization")
components = [f"Component {i+1}" for i in range(5)]
# 1. Weight Distribution Pie Chart
col1, col2 = st.columns(2)
with col1:
fig1 = px.pie(
names=components,
values=weights,
title="Weight Distribution",
color_discrete_sequence=['#8B4513', '#CFB53B', '#654321'],
hole=0.4
)
fig1.update_layout(
margin=dict(t=50, b=10),
showlegend=False
)
fig1.update_traces(
textposition='inside',
textinfo='percent+label',
marker=dict(line=dict(color='#ffffff', width=1))
)
st.plotly_chart(fig1, use_container_width=True)
# 2. Property Comparison Bar Chart
with col2:
# Property selection for fuel mode
viz_property = st.selectbox(
"Select Property to View",
[f"Property{i+1}" for i in range(10)],
key="viz_property"
)
bar_values = [p[viz_property] for p in props]
blended_value = 123 #Modify
fig2 = px.bar(
x=components,
y=bar_values,
title=f"{viz_property} Values",
color=bar_values,
color_continuous_scale='YlOrBr'
)
fig2.update_layout(
yaxis_title=viz_property,
xaxis_title="Component",
margin=dict(t=50, b=10),
coloraxis_showscale=False
)
fig2.add_hline(
y=blended_value,
line_dash="dot",
line_color="#ff6600",
annotation_text="Blended Value",
annotation_position="top right"
)
st.plotly_chart(fig2, use_container_width=True)
# Display the calculated value prominently
st.markdown(f"""
<div style="
background-color: #FAF3E6;
border-left: 4px solid #8B4513;
border-radius: 4px;
padding: 12px;
margin: 12px 0;
">
<p style="margin: 0; color: #654321;
font-size: 2.2rem;
font-weight: 800;
color: #000;
text-align:center;">
Calculated <strong>{viz_property}</strong> =
<strong style="color: #000">{blended_value:.4f}</strong>
</p>
</div>
""", unsafe_allow_html=True)
else:
# Batch mode visualization placeholder
st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
st.subheader("Batch Processing Results")
st.dataframe(data_input, use_container_width=True)
# st.info("Batch processing complete. Add custom visualizations here.")
with tabs[2]:
st.subheader("πŸ“€ Nothing FOr NOw")
# uploaded_file = st.file_uploader("Upload CSV File", type=["csv"])
# if uploaded_file:
# df = pd.read_csv(uploaded_file)
# st.success("File uploaded successfully")
# st.dataframe(df.head())
# if st.button("βš™οΈ Run Batch Prediction"):
# result_df = df.copy()
# # result_df["Predicted_Property"] = df.apply(
# # lambda row: run_dummy_prediction(row.values[:5], row.values[5:10]), axis=1
# # )
# st.success("Batch prediction completed")
# st.dataframe(result_df.head())
# csv = result_df.to_csv(index=False).encode("utf-8")
# st.download_button("Download Results", csv, "prediction_results.csv", "text/csv")
with tabs[3]:
st.subheader("βš™οΈ Optimization Engine")
# Pareto frontier demo
st.markdown("#### Cost vs Performance Trade-off")
np.random.seed(42)
optimization_data = pd.DataFrame({
'Cost ($/ton)': np.random.uniform(100, 300, 50),
'Performance Score': np.random.uniform(70, 95, 50)
})
fig3 = px.scatter(
optimization_data,
x='Cost ($/ton)',
y='Performance Score',
title="Potential Blend Formulations",
color='Performance Score',
color_continuous_scale='YlOrBr'
)
# Add dummy pareto frontier
x_pareto = np.linspace(100, 300, 10)
y_pareto = 95 - 0.1*(x_pareto-100)
fig3.add_trace(px.line(
x=x_pareto,
y=y_pareto,
color_discrete_sequence= ['#8B4513', '#CFB53B', '#654321']
).data[0])
fig3.update_layout(
showlegend=False,
annotations=[
dict(
x=200,
y=88,
text="Pareto Frontier",
showarrow=True,
arrowhead=1,
ax=-50,
ay=-30
)
]
)
st.plotly_chart(fig3, use_container_width=True)
# Blend optimization history
st.markdown("#### Optimization Progress")
iterations = np.arange(20)
performance = np.concatenate([np.linspace(70, 85, 10), np.linspace(85, 89, 10)])
fig4 = px.line(
x=iterations,
y=performance,
title="Best Performance by Iteration",
markers=True
)
fig4.update_traces(
line_color='#1d3b58',
marker_color='#2c5282',
line_width=2.5
)
fig4.update_layout(
yaxis_title="Performance Score",
xaxis_title="Iteration"
)
st.plotly_chart(fig4, use_container_width=True)
with tabs[4]:
st.subheader("πŸ“š Fuel Registry") # Changed to book emoji for registry
# Button to add new fuel
st.markdown("#### βž• Add a New Fuel Type")
with st.expander("Click to Add New Fuel", expanded=False):
with st.form("new_fuel_form", clear_on_submit=False):
fuel_name = st.text_input("Fuel Name", placeholder="e.g. Bioethanol")
cols = st.columns(5)
properties = {}
for i in range(10):
with cols[i % 5]:
prop_val = st.number_input(
f"Property {i+1}",
min_value=0.0,
step=0.1,
key=f"prop_{i}",
format="%.2f"
)
properties[f"Property{i+1}"] = round(prop_val, 2)
col1, col2 = st.columns(2)
with col1:
submitted = st.form_submit_button("πŸ’Ύ Save Fuel", use_container_width=True)
with col2:
cancelled = st.form_submit_button("❌ Cancel", use_container_width=True)
if submitted:
if not fuel_name.strip():
st.warning("Fuel name cannot be empty.")
elif fuel_name in st.session_state.FUEL_PROPERTIES:
st.error(f"{fuel_name} already exists in registry.")
else:
# Update both session state and CSV
st.session_state.FUEL_PROPERTIES[fuel_name] = properties
save_fuel_data()
st.success(f"{fuel_name} successfully added!")
st.rerun() # Refresh to show new fuel
if cancelled:
st.rerun()
with st.expander("Batch Add New Fuel", expanded=False):
uploaded_file = st.file_uploader(
"πŸ“€ Upload Fuel Batch (CSV)",
type=['csv'],
accept_multiple_files=False,
key="fuel_uploader",
help="Upload a CSV file with the same format as the exported registry"
)
if uploaded_file is not None:
try:
new_fuels = pd.read_csv(uploaded_file, index_col=0).to_dict('index')
# Check for duplicates
duplicates = [name for name in new_fuels if name in st.session_state.FUEL_PROPERTIES]
if duplicates:
st.warning(f"These fuels already exist and won't be updated: {', '.join(duplicates)}")
# Only add new fuels
new_fuels = {name: props for name, props in new_fuels.items()
if name not in st.session_state.FUEL_PROPERTIES}
if new_fuels:
st.session_state.FUEL_PROPERTIES.update(new_fuels)
save_fuel_data()
st.success(f"Added {len(new_fuels)} new fuel(s) to registry!")
st.rerun()
else:
st.info("No new fuels to add from the uploaded file.")
except Exception as e:
st.error(f"Error processing file: {str(e)}")
st.error("Please ensure the file matches the expected format")
# Display current fuel properties
st.markdown("#### πŸ” Current Fuel Properties")
st.dataframe(
pd.DataFrame(st.session_state.FUEL_PROPERTIES).T.style
.background_gradient(cmap="YlOrBr", axis=None)
.format(precision=2),
use_container_width=True,
height=(len(st.session_state.FUEL_PROPERTIES) + 1) * 35 + 3,
hide_index=False
)
# File operations section
st.download_button(
label="πŸ“₯ Download Registry (CSV)",
data=pd.DataFrame(st.session_state.FUEL_PROPERTIES).T.to_csv().encode('utf-8'),
file_name='fuel_properties.csv',
mime='text/csv',
# use_container_width=True
)
with tabs[5]:
st.subheader("🧠 Model Insights")
# Feature importance
st.markdown("#### Property Importance")
features = ['Property 1', 'Property 2', 'Property 3', 'Property 4', 'Property 5']
importance = np.array([0.35, 0.25, 0.2, 0.15, 0.05])
fig5 = px.bar(
x=importance,
y=features,
orientation='h',
title="Feature Importance for Blend Prediction",
color=importance,
color_continuous_scale='YlOrBr'
)
fig5.update_layout(
xaxis_title="Importance Score",
yaxis_title="Property",
coloraxis_showscale=False
)
st.plotly_chart(fig5, use_container_width=True)
# SHAP values demo
st.markdown("#### Property Impact Direction")
fig6 = px.scatter(
x=np.random.randn(100),
y=np.random.randn(100),
color=np.random.choice(features, 100),
title="SHAP Values (Simulated)",
labels={'x': 'Impact on Prediction', 'y': 'Property Value'}
)
fig6.update_traces(
marker=dict(size=10, opacity=0.7),
selector=dict(mode='markers')
)
fig6.add_vline(x=0, line_width=1, line_dash="dash")
st.plotly_chart(fig6, use_container_width=True)
# st.markdown("""
# <style>
# /* Consistent chart styling */
# .stPlotlyChart {
# border-radius: 10px;
# background: white;
# padding: 15px;
# box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
# margin-bottom: 25px;
# }
# /* Better select widget alignment */
# .stSelectbox > div {
# margin-bottom: -15px;
# }
# /* Color scale adjustments */
# .plotly .colorbar {
# padding: 10px !important;
# }
# </style>
# """, unsafe_allow_html=True)