Eagle / test_app.py
wayne-chi's picture
Upload 3 files
7b77ff0 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 sqlite3
from typing import Optional, Dict, Any
from datetime import datetime, timedelta
import re
from pathlib import Path
import time # Add this import to the top of your script
import math
import plotly.graph_objects as go
from pymoo.core.problem import Problem
from pymoo.core.repair import Repair
from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.optimize import minimize
from pymoo.core.callback import Callback
## -------------------------------------------------------------------------------------------------------------------------------------------
## Functions
##---------------------------------------------------------------------------------------------------------------------------------------------
class OptimizationProgressCallback(Callback):
def __init__(self, progress_bar, total_generations):
super().__init__()
self.progress_bar = progress_bar
self.total_generations = total_generations
def notify(self, algorithm):
# Calculate progress percentage
progress_percent = algorithm.n_gen / self.total_generations
# --- FIX: Changed the text to show percentage ---
progress_text = f"Optimizing... {progress_percent:.0%}"
# Update the Streamlit progress bar
self.progress_bar.progress(progress_percent, text=progress_text)
class NormalizationRepair(Repair):
"""Ensures the first 5 variables (fractions) sum to 1."""
def _do(self, problem, X, **kwargs):
fractions = X[:, :5]
row_sums = np.sum(fractions, axis=1, keepdims=True)
row_sums[row_sums == 0] = 1
normalized_fractions = fractions / row_sums
X[:, :5] = normalized_fractions
return X
class BlendOptimizationProblem(Problem):
def __init__(self, blend_model, target_properties, fixed_inputs,
frozen_targets, input_columns, output_columns, optimize_cost=False):
self.blend_model = blend_model
self.target_properties = target_properties
self.fixed_inputs = fixed_inputs
self.frozen_targets = frozen_targets
self.input_columns = input_columns
self.output_columns = output_columns # Columns from predict_fast
self.optimize_cost = optimize_cost
n_constraints = len(self.frozen_targets)
n_obj = 2 if self.optimize_cost else 1
super().__init__(n_var=5, n_obj=n_obj, n_constr=n_constraints if n_constraints > 0 else 1, xl=0.0, xu=1.0)
self.epsilon = 1e-3
self.frozen_indices_predicted = [self.output_columns.index(f'BlendProperty{k}') for k in self.frozen_targets.keys() if f'BlendProperty{k}' in self.output_columns]
self.frozen_values = np.array([v for k, v in self.frozen_targets.items() if f'BlendProperty{k}' in self.output_columns])
self.objective_mask_predicted = np.ones(len(self.output_columns), dtype=bool)
if self.frozen_indices_predicted:
self.objective_mask_predicted[self.frozen_indices_predicted] = False
all_output_prop_indices = [int(col.replace('BlendProperty', '')) for col in self.output_columns]
self.objective_targets = np.array([
self.target_properties[prop_idx - 1]
for i, prop_idx in enumerate(all_output_prop_indices)
if self.objective_mask_predicted[i]
])
def _evaluate(self, x, out, *args, **kwargs):
fixed_data = np.tile(self.fixed_inputs, (len(x), 1))
full_input_data = np.hstack([x, fixed_data])
input_df = pd.DataFrame(full_input_data, columns=self.input_columns)
# STEP 1: Use predict_fast for optimization loop
predicted_properties = self.blend_model.predict_fast(input_df)[self.output_columns].values
error = np.sum((predicted_properties[:, self.objective_mask_predicted] - self.objective_targets)**2, axis=1)
if self.optimize_cost:
component_costs = np.array([st.session_state.get(f"opt_c{i}_cost", 0.0) for i in range(5)])
cost = input_df.iloc[:, :5].values @ component_costs
out["F"] = np.column_stack([error, cost])
else:
out["F"] = error
if self.frozen_targets and self.frozen_indices_predicted:
frozen_violations = np.abs(predicted_properties[:, self.frozen_indices_predicted] - self.frozen_values) - self.epsilon
out["G"] = frozen_violations
else:
out["G"] = np.zeros(len(x))
# def run_real_optimization(targets, fixed_targets, components_data, include_cost):
# """Main function to run the pymoo optimization."""
# # 1. SETUP
# blend_model = st.session_state.predictor
# # All 55 input columns (5 fractions + 50 properties)
# input_cols = [f'Component{i+1}_fraction' for i in range(5)]
# for j in range(1, 11):
# for i in range(1, 6):
# input_cols.append(f'Component{i}_Property{j}')
# # The 50 fixed property values from the UI
# fixed_model_inputs = []
# for j in range(1, 11):
# for i in range(5):
# fixed_model_inputs.append(st.session_state.get(f"opt_c{i}_prop{j}", 0.0))
# fixed_model_inputs = np.array(fixed_model_inputs)
# # STEP 2: Optimize based on only the 6 specified properties
# output_cols_fast = [f'BlendProperty{i}' for i in [1, 2, 5, 6, 7, 10]]
# full_target_properties = np.array(list(targets.values()))
# frozen_targets_to_use = {int(k.replace('Property', '')): v for k, v in fixed_targets.items()}
# # 2. RUN OPTIMIZATION
# problem = BlendOptimizationProblem(
# blend_model=blend_model, target_properties=full_target_properties,
# fixed_inputs=fixed_model_inputs, frozen_targets=frozen_targets_to_use,
# input_columns=input_cols, output_columns=output_cols_fast,
# optimize_cost=include_cost
# )
# algorithm = NSGA2(pop_size=100, repair=NormalizationRepair(), eliminate_duplicates=True)
# res = minimize(problem, algorithm, termination=('n_gen', 50), seed=1, verbose=False)
# # 3. PROCESS AND RETURN RESULTS
# if res.X is None or len(res.X) == 0:
# st.error("Optimization failed to find a feasible solution. Consider relaxing your constraints.")
# return []
# # Prepare a full input DataFrame to get all 10 properties for the UI display
# final_fractions_df = pd.DataFrame(res.X, columns=[f'Component{i+1}_fraction' for i in range(5)])
# fixed_df_part = pd.DataFrame([fixed_model_inputs] * len(final_fractions_df), columns=input_cols[5:])
# full_input_for_final_pred = pd.concat([final_fractions_df, fixed_df_part], axis=1)
# # Use predict_all to get the full 10 properties for the UI, ensuring compatibility
# all_10_properties_df = blend_model.predict_all(full_input_for_final_pred)
# solutions = []
# for i in range(len(res.X)):
# solution_data = {
# "component_fractions": res.X[i],
# "blend_properties": all_10_properties_df.iloc[i].values, # Full 10 properties
# "error": res.F[i][0],
# "optimized_cost": res.F[i][1] if include_cost else 0.0
# }
# solutions.append(solution_data)
# return solutions
def run_real_optimization(targets, fixed_targets, components_data, include_cost, generations, pop_size, progress_bar):
"""Main function to run the pymoo optimization."""
# 1. SETUP (Remains the same)
blend_model = st.session_state.predictor
input_cols = [f'Component{i+1}_fraction' for i in range(5)]
for j in range(1, 11):
for i in range(1, 6):
input_cols.append(f'Component{i}_Property{j}')
fixed_model_inputs = []
for j in range(1, 11):
for i in range(5):
fixed_model_inputs.append(st.session_state.get(f"opt_c{i}_prop{j}", 0.0))
fixed_model_inputs = np.array(fixed_model_inputs)
output_cols_fast = [f'BlendProperty{i}' for i in [1, 2, 5, 6, 7, 10]]
full_target_properties = np.array(list(targets.values()))
frozen_targets_to_use = {int(k.replace('Property', '')): v for k, v in fixed_targets.items()}
# 2. RUN OPTIMIZATION (Remains the same)
problem = BlendOptimizationProblem(
blend_model=blend_model, target_properties=full_target_properties,
fixed_inputs=fixed_model_inputs, frozen_targets=frozen_targets_to_use,
input_columns=input_cols, output_columns=output_cols_fast,
optimize_cost=include_cost
)
algorithm = NSGA2(pop_size=pop_size, repair=NormalizationRepair(), eliminate_duplicates=True)
# Instantiate the callback with the progress bar and total generations
callback = OptimizationProgressCallback(progress_bar, generations)
# Add the 'callback' argument to the minimize function
res = minimize(problem, algorithm, termination=('n_gen', generations), seed=1, verbose=False, callback=callback)
# 3. PROCESS AND RETURN RESULTS (This section is modified)
if res.X is None or len(res.X) == 0:
st.error("Optimization failed to find a feasible solution. Consider relaxing your constraints.")
return []
# --- FIX: NO predict_all(). Instead, we build the final property list manually. ---
# First, get the final *predicted* values for the 6 optimized properties
final_fractions_df = pd.DataFrame(res.X, columns=[f'Component{i+1}_fraction' for i in range(5)])
fixed_df_part = pd.DataFrame([fixed_model_inputs] * len(final_fractions_df), columns=input_cols[5:])
full_input_for_fast_pred = pd.concat([final_fractions_df, fixed_df_part], axis=1)
predicted_6_properties_df = blend_model.predict_fast(full_input_for_fast_pred)
solutions = []
optimized_prop_indices = [1, 2, 5, 6, 7, 10]
for i in range(len(res.X)):
# Create a 10-element array for the UI
final_10_properties = np.zeros(10)
for prop_idx in range(1, 11):
if prop_idx in optimized_prop_indices:
# For the 6 optimized properties, use the value from predict_fast
col_name = f'BlendProperty{prop_idx}'
final_10_properties[prop_idx - 1] = predicted_6_properties_df[col_name].iloc[i]
else:
# For the other 4, use the user's original target value as a placeholder
final_10_properties[prop_idx - 1] = targets[f'Property{prop_idx}']
solution_data = {
"component_fractions": res.X[i],
"blend_properties": final_10_properties, # Use the manually constructed array
"error": res.F[i][0],
"optimized_cost": res.F[i][1] if include_cost else 0.0
}
solutions.append(solution_data)
solutions.sort(key=lambda x: x.get('error', float('inf')))
return solutions
def calculate_quality_score(error, tolerance=1e-3):
"""Calculates a quality score, handling potential math errors."""
if error is None or tolerance <= 0:
return 0.0
# If error is high, score is 0 to avoid math errors with log.
if error >= tolerance:
return 0.0
# Prevent division by zero if error equals tolerance exactly in floating point.
ratio = error / tolerance
if ratio >= 1.0:
return 0.0
try:
# The core formula
score = 100 * (1 / (1 + math.log(1 - ratio)))
# Ensure score is capped between 0 and 100
return max(0, min(100, score))
except (ValueError, TypeError):
# Catch any other unexpected math errors
return 0.0
@st.cache_data
def get_all_blends_data(db_path="eagleblend.db") -> pd.DataFrame:
"""Fetches all blend data, sorted by the most recent entries."""
with sqlite3.connect(db_path) as conn:
# Assuming 'id' is the primary key indicating recency
query = "SELECT * FROM blends ORDER BY id DESC"
df = pd.read_sql_query(query, conn)
return df
def filter_component_options(df: pd.DataFrame, component_index: int) -> list:
"""
Filters component options for a dropdown.
- Primary filter: by 'component_type' matching the component index + 1.
- Fallback filter: by 'component_name' ending with '_Component_{index+1}'.
"""
target_type = component_index + 1
# Primary Filter: Use 'component_type' if the column exists and has data.
if 'component_type' in df.columns and not df['component_type'].isnull().all():
# Use .loc to avoid SettingWithCopyWarning
filtered_df = df.loc[df['component_type'] == target_type]
if not filtered_df.empty:
return filtered_df['component_name'].tolist()
# Fallback Filter: If the primary filter fails or doesn't apply, use the name.
# The 'na=False' gracefully handles any nulls in the component_name column.
fallback_df = df.loc[df['component_name'].str.endswith(f"_Component_{target_type}", na=False)]
return fallback_df['component_name'].tolist()
# ---------------------- Page Config ----------------------
st.set_page_config(
layout="wide",
page_title="Eagle Blend Optimizer",
page_icon="🦅",
initial_sidebar_state="collapsed"
)
# ---------------------- Sidebar Content ----------------------
with st.sidebar:
st.markdown("---")
st.markdown("### 🦅 Developed by eagle-team")
st.markdown("""
- Destiny Otto
- Williams Alabi
- Godswill Otto
- Alexander Ifenaike
""")
st.markdown("---")
st.info("Select a tab above to get started.")
# ---------------------- Custom Styling ---------------------- ##e0e0e0;
st.markdown("""
<style>
.block-container {
padding-top: 1rem;
}
/* Main app background */
.stApp {
background-color: #f8f5f0;
overflow: visible;
padding-top: 0
/* --- ADD THIS CSS FOR THE NEW HELP BUTTONS --- */
#help-toggle-insights:checked ~ .help-panel-insights,
#help-toggle-registry:checked ~ .help-panel-registry,
#help-toggle-comparison:checked ~ .help-panel-comparison {
opacity: 1; visibility: visible; transform: translateY(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 --- THIS IS THE FIX --- */
.stPlotlyChart {
border-radius: 10px;
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;
}
/* --- Add this CSS class for the spinner --- */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.spinner {
border: 4px solid rgba(0,0,0,0.1);
border-left-color: #8B4513;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
/* Color scale adjustments */
.plotly .colorbar {
padding: 10px !important;
color: #654321 !important;
}
</style>
""", unsafe_allow_html=True)
# ---------------------- App Header ----------------------
# --- This is the new header with the subtitle ---
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>
<p style='text-align: center; font-weight: 300; font-size: 1rem; margin-top: 0.75rem; opacity: 0.9;'>
by <b>eagle-team</b> for the Shell.ai 2025 Hackathon
</p>
</div>
""", unsafe_allow_html=True)
#------ universal variables
# ---------------------- Tabs ----------------------
tabs = st.tabs([
"📊 Dashboard",
"🎛️ Blend Designer",
"⚙️ Optimization Engine",
"📤 Blend Comparison",
"📚 Fuel Registry",
"🧠 Model Insights"
])
def explode_blends_to_components(blends_df: pd.DataFrame,
n_components: int = 5,
keep_empty: bool = False,
blend_name_col: str = "blend_name") -> pd.DataFrame:
"""
Convert a blends DataFrame into a components DataFrame.
Parameters
----------
blends_df : pd.DataFrame
DataFrame with columns following the pattern:
Component1_fraction, Component1_Property1..Property10, Component1_unit_cost, ...
n_components : int
Number of components per blend (default 5).
blend_name_col : str
Column name in blends_df that stores the blend name.
Returns
-------
pd.DataFrame
components_df with columns:
['blend_name', 'component_name', 'component_fraction',
'property1', ..., 'property10', 'unit_cost']
"""
components_rows = []
prop_names = [f"property{i}" for i in range(1, 11)]
for _, blend_row in blends_df.iterrows():
blend_name = blend_row.get(blend_name_col)
# Fallback if blend_name is missing/empty - keep index-based fallback
if not blend_name or str(blend_name).strip() == "":
# use the dataframe index + 1 to create a fallback name
blend_name = f"blend{int(blend_row.name) + 1}"
for i in range(1, n_components + 1):
# Build column keys
frac_col = f"Component{i}_fraction"
unit_cost_col = f"Component{i}_unit_cost"
prop_cols = [f"Component{i}_Property{j}" for j in range(1, 11)]
# Safely get values (if column missing, get NaN)
comp_frac = blend_row.get(frac_col, np.nan)
comp_unit_cost = blend_row.get(unit_cost_col, np.nan)
comp_props = [blend_row.get(pc, np.nan) for pc in prop_cols]
row = {
"blend_name": blend_name,
"component_name": f"{blend_name}_Component_{i}",
"component_fraction": comp_frac,
"component_type": i,
"unit_cost": comp_unit_cost
}
# add property1..property10
for j, v in enumerate(comp_props, start=1):
row[f"property{j}"] = v
components_rows.append(row)
components_df = pd.DataFrame(components_rows)
return components_df
# --- Updated add_blends (now also populates components) ---
def add_blends(df, db_path="eagleblend.db", n_components=5):
df = df.copy()
# 1) Ensure blend_name column
for col in list(df.columns):
low = col.strip().lower()
if low in ("blend_name", "blend name", "blendname"):
if col != "blend_name":
df = df.rename(columns={col: "blend_name"})
break
if "blend_name" not in df.columns:
df["blend_name"] = pd.NA
conn = sqlite3.connect(db_path)
cur = conn.cursor()
# 2) Determine next blend number
cur.execute("SELECT blend_name FROM blends WHERE blend_name LIKE 'blend%'")
nums = [int(m.group(1)) for (b,) in cur.fetchall() if (m := re.match(r"blend(\d+)$", str(b)))]
start_num = max(nums) if nums else 0
# 3) Fill missing blend_name
mask = df["blend_name"].isna() | (df["blend_name"].astype(str).str.strip() == "")
df.loc[mask, "blend_name"] = [f"blend{i}" for i in range(start_num + 1, start_num + 1 + mask.sum())]
# 4) Safe insert into blends
cur.execute("PRAGMA table_info(blends)")
db_cols = [r[1] for r in cur.fetchall()]
safe_df = df[[c for c in df.columns if c in db_cols]]
if not safe_df.empty:
safe_df.to_sql("blends", conn, if_exists="append", index=False)
# 5) Explode blends into components and insert into components table
components_df = explode_blends_to_components(df, n_components=n_components, keep_empty=False)
cur.execute("PRAGMA table_info(components)")
comp_cols = [r[1] for r in cur.fetchall()]
safe_components_df = components_df[[c for c in components_df.columns if c in comp_cols]]
if not safe_components_df.empty:
safe_components_df.to_sql("components", conn, if_exists="append", index=False)
conn.commit()
conn.close()
return {
"blends_inserted": int(safe_df.shape[0]),
"components_inserted": int(safe_components_df.shape[0])
}
# --- add_components function ---
def add_components(df, db_path="eagleblend.db"):
df = df.copy()
# Ensure blend_name exists
for col in list(df.columns):
low = col.strip().lower()
if low in ("blend_name", "blend name", "blendname"):
if col != "blend_name":
df = df.rename(columns={col: "blend_name"})
break
if "blend_name" not in df.columns:
df["blend_name"] = pd.NA
# Ensure component_name exists
if "component_name" not in df.columns:
df["component_name"] = pd.NA
conn = sqlite3.connect(db_path)
cur = conn.cursor()
# Fill missing component_name
mask = df["component_name"].isna() | (df["component_name"].astype(str).str.strip() == "")
df.loc[mask, "component_name"] = [
f"{bn}_Component_{i+1}"
for i, bn in enumerate(df["blend_name"].fillna("blend_unknown"))
]
# Safe insert into components
cur.execute("PRAGMA table_info(components)")
db_cols = [r[1] for r in cur.fetchall()]
safe_df = df[[c for c in df.columns if c in db_cols]]
if not safe_df.empty:
safe_df.to_sql("components", conn, if_exists="append", index=False)
conn.commit()
conn.close()
return int(safe_df.shape[0])
def get_blends_overview(db_path: str = "eagleblend.db", last_n: int = 5) -> Dict[str, Any]:
"""
Returns:
{
"max_saving": float | None, # raw numeric (PreOpt_Cost - Optimized_Cost)
"last_blends": pandas.DataFrame, # last_n rows of selected columns
"daily_counts": pandas.Series # counts per day, index = 'YYYY-MM-DD' (strings)
}
"""
last_n = int(last_n)
comp_cols = [
"blend_name", "Component1_fraction", "Component2_fraction", "Component3_fraction",
"Component4_fraction", "Component5_fraction", "created_at"
]
blend_props = [f"BlendProperty{i}" for i in range(1, 11)]
select_cols = comp_cols + blend_props
cols_sql = ", ".join(select_cols)
with sqlite3.connect(db_path) as conn:
# 1) scalar: max saving
max_saving = conn.execute(
"SELECT MAX(PreOpt_Cost - Optimized_Cost) "
"FROM blends "
"WHERE PreOpt_Cost IS NOT NULL AND Optimized_Cost IS NOT NULL"
).fetchone()[0]
# 2) last N rows (only selected columns)
q_last = f"""
SELECT {cols_sql}
FROM blends
ORDER BY id DESC
LIMIT {last_n}
"""
df_last = pd.read_sql_query(q_last, conn)
# 3) daily counts (group by date)
q_counts = """
SELECT date(created_at) AS day, COUNT(*) AS cnt
FROM blends
WHERE created_at IS NOT NULL
GROUP BY day
ORDER BY day DESC
"""
df_counts = pd.read_sql_query(q_counts, conn)
# Convert counts to a Series with day strings as index (fast, small memory)
if not df_counts.empty:
daily_counts = pd.Series(df_counts["cnt"].values, index=df_counts["day"].astype(str))
daily_counts.index.name = "day"
daily_counts.name = "count"
else:
daily_counts = pd.Series(dtype=int, name="count")
return {"max_saving": max_saving, "last_blends": df_last, "daily_counts": daily_counts}
def get_activity_logs(db_path="eagleblend.db", timeframe="today", activity_type=None):
"""
Get counts of activities from the activity_log table within a specified timeframe.
Args:
db_path (str): Path to the SQLite database file.
timeframe (str): Time period to filter ('today', 'this_week', 'this_month', or 'custom').
activity_type (str): Specific activity type to return count for. If None, return all counts.
Returns:
dict: Dictionary with counts per activity type OR a single integer if activity_type is specified.
"""
# Calculate time filter
now = datetime.now()
if timeframe == "today":
start_time = now.replace(hour=0, minute=0, second=0, microsecond=0)
elif timeframe == "this_week":
start_time = now - timedelta(days=now.weekday()) # Monday of this week
start_time = start_time.replace(hour=0, minute=0, second=0, microsecond=0)
elif timeframe == "this_month":
start_time = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
else:
raise ValueError("Invalid timeframe. Use 'today', 'this_week', or 'this_month'.")
# Query database
conn = sqlite3.connect(db_path)
query = f"""
SELECT activity_type, COUNT(*) as count
FROM activity_log
WHERE timestamp >= ?
GROUP BY activity_type
"""
df_counts = pd.read_sql_query(query, conn, params=(start_time.strftime("%Y-%m-%d %H:%M:%S"),))
conn.close()
# Convert to dictionary
counts_dict = dict(zip(df_counts["activity_type"], df_counts["count"]))
# If specific activity requested
if activity_type:
return counts_dict.get(activity_type, 0)
return counts_dict
# print(get_activity_logs(timeframe="today")) # All activities today
# print(get_activity_logs(timeframe="this_week")) # All activities this week
# print(get_activity_logs(timeframe="today", activity_type="optimization")) # Only optimization count today
# result = get_activity_logs(timeframe="this_week")
# result['optimization']
# result['prediction']
def get_model(db_path="eagleblend.db"):
"""
Fetch the last model from the models_registry table.
Returns:
pandas.Series: A single row containing the last model's data.
"""
conn = sqlite3.connect(db_path)
query = "SELECT * FROM models_registry ORDER BY id DESC LIMIT 1"
df_last = pd.read_sql_query(query, conn)
conn.close()
if not df_last.empty:
return df_last.iloc[0] # Return as a Series so you can access columns easily
else:
return None
# last_model = get_model()
# if last_model is not None:
# print("R2 Score:", last_model["R2_Score"])
# ----------------------------------------------------------------------------------------------------------------------------------------------
# Dashboard Tab
# ----------------------------------------------------------------------------------------------------------------------------------------------
with tabs[0]:
# NOTE: Assuming these functions are defined elsewhere in your application
# from your_utils import get_model, get_activity_logs, get_blends_overview
# ---------- formatting helpers ----------
def fmt_int(x):
try:
return f"{int(x):,}"
except Exception:
return "0"
def fmt_pct_from_r2(r2):
if r2 is None:
return "—"
try:
v = float(r2)
if v <= 1.5:
v *= 100.0
return f"{v:.1f}%"
except Exception:
return "—"
def fmt_currency(x):
try:
return f"${float(x):,.2f}"
except Exception:
return "—"
# ---------- pull live data (this_week only) ----------
# This block is assumed to be correct and functional
try:
last_model = get_model()
except Exception as e:
last_model = None
st.warning(f"Model lookup failed: {e}")
try:
activity_counts = get_activity_logs(timeframe="this_week")
except Exception as e:
activity_counts = {}
st.warning(f"Activity log lookup failed: {e}")
try:
overview = get_blends_overview(last_n=5)
except Exception as e:
overview = {"max_saving": None, "last_blends": pd.DataFrame(), "daily_counts": pd.Series(dtype=int)}
st.warning(f"Blends overview failed: {e}")
r2_display = fmt_pct_from_r2(None if last_model is None else last_model.get("R2_Score"))
preds = fmt_int(activity_counts.get("prediction", 0))
opts = fmt_int(activity_counts.get("optimization", 0))
max_saving_display = fmt_currency(overview.get("max_saving", None))
# ---------- KPI cards ----------
# FIXED: Replaced st.subheader with styled markdown for consistent color
st.markdown('<h2 style="color:#4a2f1f; font-size:1.75rem;">Performance Summary</h2>', unsafe_allow_html=True)
k1, k2, k3, k4 = st.columns(4)
with k1:
st.markdown(f"""
<div class="metric-card" style="padding:10px;">
<div class="metric-label">Model Accuracy</div>
<div class="metric-value" style="font-size:1.3rem;">{r2_display}</div>
<div class="metric-delta">R² (latest)</div>
</div>
""", unsafe_allow_html=True)
with k2:
st.markdown(f"""
<div class="metric-card" style="padding:10px;">
<div class="metric-label">Predictions Made</div>
<div class="metric-value" style="font-size:1.3rem;">{preds}</div>
<div class="metric-delta">This Week</div>
</div>
""", unsafe_allow_html=True)
with k3:
st.markdown(f"""
<div class="metric-card" style="padding:10px;">
<div class="metric-label">Optimizations</div>
<div class="metric-value" style="font-size:1.3rem;">{opts}</div>
<div class="metric-delta">This Week</div>
</div>
""", unsafe_allow_html=True)
with k4:
st.markdown(f"""
<div class="metric-card" style="padding:10px;">
<div class="metric-label">Highest Cost Savings</div>
<div class="metric-value" style="font-size:1.3rem;">{max_saving_display}</div>
<div class="metric-delta">Per unit fuel</div>
</div>
""", unsafe_allow_html=True)
st.markdown('<div style="height:8px;"></div>', unsafe_allow_html=True)
# ---------- Floating "How to Use" (bigger button + inline content) + compact CSS ----------
# st.markdown("""
# <style>
# /* Floating help - larger button and panel */
# #help-toggle{display:none;}
# .help-button{
# position:fixed; right:25px; bottom:25px; z-index:9999;
# background:#8B4513; color:#FFD700; padding:16px 22px; font-size:17px;
# border-radius:18px; font-weight:900; box-shadow:0 8px 22px rgba(0,0,0,0.2); cursor:pointer;
# border:0;
# }
# .help-panel{
# position:fixed; right:25px; bottom:100px; z-index:9998;
# width:520px; max-height:70vh; overflow-y:auto;
# background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
# border:1px solid #CFB53B; border-radius:12px; padding:20px; box-shadow:0 14px 34px rgba(0,0,0,0.22);
# color:#4a2f1f; transform: translateY(12px); opacity:0; visibility:hidden; transition: all .22s ease-in-out;
# }
# #help-toggle:checked + label.help-button + .help-panel{
# opacity:1; visibility:visible; transform: translateY(0);
# }
# .help-panel .head{display:flex; justify-content:space-between; align-items:center; margin-bottom:12px}
# .help-panel .title{font-weight:900; color:#654321; font-size:16px}
# .help-close{background:#8B4513; color:#FFD700; padding:6px 10px; border-radius:8px; cursor:pointer; font-weight:800}
# .help-body{font-size:14.5px; color:#4a2f1f; line-height:1.5}
# .help-body b {color: #654321;}
# /* compact recent blends styles - improved font sizes */
# .recent-compact { padding-left:6px; padding-right:6px; }
# .compact-card{
# background: linear-gradient(180deg,#FFF8E1 0%, #FFF6EA 100%);
# border:1px solid #E3C77A; border-radius:8px; padding:10px; margin-bottom:8px; color:#654321;
# box-shadow: 0 2px 6px rgba(0,0,0,0.05);
# }
# .compact-top{display:flex; justify-content:space-between; align-items:center; margin-bottom:8px}
# .compact-name{font-weight:800; font-size:15px}
# .compact-ts{font-size:12px; color:#8B4513; opacity:0.95; font-weight:700}
# .comp-pills{font-size:12.5px; margin-bottom:8px}
# .comp-pill{
# display:inline-block; padding:3px 8px; margin-right:6px; margin-bottom: 4px; border-radius:999px;
# background:rgba(139,69,19,0.06); border:1px solid rgba(139,69,19,0.12);
# font-weight:700; color:#654321;
# }
# .props-inline{
# font-size:12px; color:#4a2f1f; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
# }
# .props-inline small{ font-size:11px; color:#4a2f1f; opacity:0.95; margin-right:8px; }
# </style>
# <input id="help-toggle" type="checkbox" />
# <label for="help-toggle" class="help-button">💬 Help</label>
# <div class="help-panel" aria-hidden="true">
# <div class="head">
# <div class="title">How to Use the Optimizer</div>
# <label for="help-toggle" class="help-close">Close</label>
# </div>
# <div class="help-body">
# <p><b>Performance Cards:</b> These show key metrics at a glance. "Model Accuracy" is the latest R² score. "Predictions" and "Optimizations" cover this week's activity. If a card shows "—", the underlying data may be missing.</p>
# <p><b>Blend Entries Chart:</b> This chart tracks how many new blends are created each day. Spikes can mean heavy usage or batch imports, while gaps might point to data ingestion issues.</p>
# <p><b>Recent Blends:</b> This is a live list of the newest blends. Each card displays the blend's name, creation time, component mix (C1-C5), and key properties (P1-P10). You can use the name and timestamp to find the full record in the database.</p>
# <p><b>Operational Tips:</b> For best results, use consistent naming for your blends. Ensure your data includes cost fields for savings to be calculated correctly. Consider retraining your model if its accuracy drops.</p>
# </div>
# </div>
# """, unsafe_allow_html=True)
# --- FIX: Removed extra blank lines inside the <ul> tag to ensure all items render ---
st.markdown("""
<style>
/* Floating help - larger button and panel */
#help-toggle{display:none;}
.help-button{
position:fixed; right:25px; bottom:25px; z-index:9999;
background:#8B4513; color:#FFD700; padding:16px 22px; font-size:17px;
border-radius:18px; font-weight:900; box-shadow:0 8px 22px rgba(0,0,0,0.2); cursor:pointer;
border:0;
}
.help-panel{
position:fixed; right:25px; bottom:100px; z-index:9998;
width:520px; max-height:70vh; overflow-y:auto;
background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
border:1px solid #CFB53B; border-radius:12px; padding:20px; box-shadow:0 14px 34px rgba(0,0,0,0.22);
color:#4a2f1f; transform: translateY(12px); opacity:0; visibility:hidden; transition: all .22s ease-in-out;
}
#help-toggle:checked + label.help-button + .help-panel{
opacity:1; visibility:visible; transform: translateY(0);
}
.help-panel .head{display:flex; justify-content:space-between; align-items:center; margin-bottom:12px}
.help-panel .title{font-weight:900; color:#654321; font-size:16px}
.help-panel .help-close{background:#8B4513; color:#FFD700; padding:6px 10px; border-radius:8px; cursor:pointer; font-weight:800}
.help-body{font-size:14.5px; color:#4a2f1f; line-height:1.5}
.help-body b {color: #654321;}
.help-body ul { padding-left: 20px; }
.help-body li { margin-bottom: 8px; }
/* compact recent blends styles - improved font sizes */
.recent-compact { padding-left:6px; padding-right:6px; }
.compact-card{
background: linear-gradient(180deg,#FFF8E1 0%, #FFF6EA 100%);
border:1px solid #E3C77A; border-radius:8px; padding:10px; margin-bottom:8px; color:#654321;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
.compact-top{display:flex; justify-content:space-between; align-items:center; margin-bottom:8px}
.compact-name{font-weight:800; font-size:15px}
.compact-ts{font-size:12px; color:#8B4513; opacity:0.95; font-weight:700}
.comp-pills{font-size:12.5px; margin-bottom:8px}
.comp-pill{
display:inline-block; padding:3px 8px; margin-right:6px; margin-bottom: 4px; border-radius:999px;
background:rgba(139,69,19,0.06); border:1px solid rgba(139,69,19,0.12);
font-weight:700; color:#654321;
}
.props-inline{
font-size:12px; color:#4a2f1f; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.props-inline small{ font-size:11px; color:#4a2f1f; opacity:0.95; margin-right:8px; }
</style>
<input id="help-toggle" type="checkbox" />
<label for="help-toggle" class="help-button">💬 App Guide</label>
<div class="help-panel" aria-hidden="true">
<div class="head">
<div class="title">Welcome to the Eagle Blend Optimizer!</div>
<label for="help-toggle" class="help-close">Close</label>
</div>
<div class="help-body">
<p>This is your central hub for AI-powered fuel blend analysis, prediction, and optimization. The app is organized into several powerful tabs:</p>
<ul>
<li><b>📊 Dashboard:</b> You are here! This is your main overview, showing key metrics like model accuracy, recent app activity, and the highest cost savings achieved. The list on the right gives you a live look at the most recently created blends.</li>
<li><b>🎛️ Blend Designer:</b> This is your creative sandbox. Manually define the fractions and properties of up to five components to instantly predict the final properties of a new blend. You can also switch to <b>Batch Mode</b> to upload a CSV and predict many blends at once.</li>
<li><b>⚙️ Optimization Engine:</b> Go beyond simple prediction. Here, you set the <b>target properties</b> you want to achieve. The AI engine will then run an optimization to find the ideal component fractions that best meet your goals and constraints, such as minimizing cost.</li>
<li><b>📤 Blend Comparison:</b> This is your analysis workbench. Select up to three previously saved blends from your database to perform a detailed side-by-side comparison. The charts will help you visualize differences in their cost, composition, and performance profiles.</li>
<li><b>📚 Fuel Registry:</b> The heart of your data. This tab is where you manage the database of all raw <b>Components</b> and saved <b>Blends</b>. You can view, add, and delete records here.</li>
<li><b>🧠 Model Insights:</b> Look under the hood of the AI. This tab shows detailed performance metrics for the prediction model, helping you understand its accuracy and where its predictions are most reliable.</li>
</ul>
<hr style="border-top: 1px solid #CFB53B; margin: 15px 0;">
<p><b>Getting Started:</b> A great first step is to visit the <b>Fuel Registry</b> to see your available components, then head to the <b>Blend Designer</b> to create your first prediction!</p>
</div>
</div>
""", unsafe_allow_html=True)
# ---------- Floating "How to Use" (bigger button + inline content) + compact CSS ----------
# ---------- Main split (adjusted for better balance) ----------
left_col, right_col = st.columns([0.55, 0.45])
# --- LEFT: Blend entries line chart ---
with left_col:
# FIXED: Replaced st.subheader with styled markdown for consistent color
st.markdown('<h2 style="color:#4a2f1f; font-size:1.75rem;">Blend Entries Per Day</h2>', unsafe_allow_html=True)
# Using DUMMY DATA as per original snippet for illustration
today = pd.Timestamp.today().normalize()
dates = pd.date_range(end=today, periods=14)
ddf = pd.DataFrame({"day": dates, "Blends": np.array([2,3,1,5,6,2,4,9,3,4,2,1,5,6])})
fig_daily = go.Figure()
fig_daily.add_trace(go.Scatter(
x=ddf["day"], y=ddf["Blends"],
mode="lines+markers", line=dict(width=3, color="#8B4513"),
marker=dict(size=6), name="Blends"
))
fig_daily.add_trace(go.Scatter(
x=ddf["day"], y=ddf["Blends"],
mode="lines", line=dict(width=0), fill="tozeroy",
fillcolor="rgba(207,181,59,0.23)", showlegend=False
))
fig_daily.update_layout(
title="Recent Blend Creation (preview)",
xaxis_title="Date", yaxis_title="Number of Blends",
plot_bgcolor="white", paper_bgcolor="white", # Set background to white
margin=dict(t=40, r=10, b=36, l=50), # Tighter margins
font=dict(color="#4a2f1f") # Ensure text color is not white
)
fig_daily.update_xaxes(gridcolor="rgba(139,69,19,0.12)", tickfont=dict(color="#654321"))
fig_daily.update_yaxes(gridcolor="rgba(139,69,19,0.12)", tickfont=dict(color="#654321"))
st.plotly_chart(fig_daily, use_container_width=True)
# st.caption("Chart preview uses dummy data. To show live counts, uncomment the LIVE DATA block in the code.")
# --- RIGHT: Compact Recent Blends (with larger fonts and clear timestamp) ---
with right_col:
st.markdown('<div class="recent-compact">', unsafe_allow_html=True)
st.markdown('<div style="font-size: 1.15rem; font-weight:800; color:#654321; margin-bottom:12px;">🗒️ Recent Blends</div>', unsafe_allow_html=True)
df_recent = overview['last_blends'] #get("last_blends", pd.DataFrame())
if df_recent is None or df_recent.empty:
st.info("No blends yet. Start blending today!")
else:
if "created_at" in df_recent.columns and not pd.api.types.is_datetime64_any_dtype(df_recent["created_at"]):
with pd.option_context('mode.chained_assignment', None):
df_recent["created_at"] = pd.to_datetime(df_recent["created_at"], errors="coerce")
for _, row in df_recent.iterrows():
name = str(row.get("blend_name", "Untitled"))
created = row.get("created_at", "")
ts = "" if pd.isna(created) else pd.to_datetime(created).strftime("%Y-%m-%d %H:%M:%S")
comp_html = ""
for i in range(1, 6):
key = f"Component{i}_fraction"
val = row.get(key)
if val is None or (isinstance(val, float) and math.isnan(val)) or val == 0:
continue
comp_html += f'<span class="comp-pill">C{i}: {float(val)*100:.0f}%</span>'
props = []
for j in range(1, 11):
pj = row.get(f"BlendProperty{j}")
if pj is not None and not (isinstance(pj, float) and math.isnan(pj)):
props.append(f"P{j}:{float(pj):.3f}")
props_html = " · ".join(props) if props else "No properties available."
st.markdown(f"""
<div class="compact-card">
<div class="compact-top">
<div class="compact-name">{name}</div>
<div class="compact-ts">{ts}</div>
</div>
<div class="comp-pills">{comp_html}</div>
<div class="props-inline"><small>{props_html}</small></div>
</div>
""", unsafe_allow_html=True)
st.markdown('</div>', unsafe_allow_html=True)
# ----------------------------------------------------------------------------------------------------------------------------------------------
# Blend Designer Tab
# ----------------------------------------------------------------------------------------------------------------------------------------------
# --- Add these new functions to your functions section ---
@st.cache_data
def get_components_from_db(db_path="eagleblend.db") -> pd.DataFrame:
"""Fetches component data, sorted by the most recent entries."""
with sqlite3.connect(db_path) as conn:
# Assuming 'id' or a timestamp column indicates recency. Let's use 'id'.
query = "SELECT * FROM components ORDER BY id DESC"
df = pd.read_sql_query(query, conn)
return df
def log_activity(activity_type: str, details: str = "", db_path="eagleblend.db"):
"""Logs an activity to the activity_log table."""
try:
with sqlite3.connect(db_path) as conn:
cur = conn.cursor()
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cur.execute(
"INSERT INTO activity_log (timestamp, activity_type) VALUES (?, ?)",
(timestamp, activity_type)
)
conn.commit()
except Exception as e:
st.error(f"Failed to log activity: {e}")
# Instantiate the predictor once
@st.cache_resource
def load_model():
from predictor import EagleBlendPredictor
# heavy model load...
return EagleBlendPredictor()
if 'predictor' not in st.session_state:
st.session_state.predictor = load_model()
with tabs[1]:
# --- State Initialization ---
if 'prediction_made' not in st.session_state:
st.session_state.prediction_made = False
if 'prediction_results' not in st.session_state:
st.session_state.prediction_results = None
if 'preopt_cost' not in st.session_state:
st.session_state.preopt_cost = 0.0
if 'last_input_data' not in st.session_state:
st.session_state.last_input_data = {}
# --- Prediction & Saving Logic ---
def handle_prediction():
"""
Gathers data from UI, formats it, runs prediction, and stores results.
"""
log_activity("prediction", "User ran a new blend prediction.")
fractions = []
properties_by_comp = [[] for _ in range(5)]
unit_costs = []
# 1. Gather all inputs from session state
for i in range(5):
frac = st.session_state.get(f"c{i}_fraction", 0.0)
fractions.append(frac)
unit_costs.append(st.session_state.get(f"c{i}_cost", 0.0))
for j in range(1, 11):
prop = st.session_state.get(f"c{i}_prop{j}", 0.0)
properties_by_comp[i].append(prop)
# 2. Validate weights
if abs(sum(fractions) - 1.0) > 0.01:
st.warning("⚠️ Total of component fractions must sum to 1.0.")
st.session_state.prediction_made = False
return
# 3. Format DataFrame for the model
model_input_data = {"blend_name": [st.session_state.get("blend_name", "Untitled Blend")]}
# Add fractions first
for i in range(5):
model_input_data[f'Component{i+1}_fraction'] = [fractions[i]]
# Add properties in the required order (interleaved)
for j in range(10): # Property1, Property2, ...
for i in range(5): # Component1, Component2, ...
col_name = f'Component{i+1}_Property{j+1}'
model_input_data[col_name] = [properties_by_comp[i][j]]
df_model = pd.DataFrame(model_input_data)
# 4. Run prediction
predictor = st.session_state.predictor
# results = predictor.predict_all(df_model.drop(columns=['blend_name']))
# st.session_state.prediction_results = results[0] # Get the first (and only) row of results
# --- FIX: Handles DataFrame output and converts it to an array for single prediction ---
results_df = predictor.predict_all(df_model.drop(columns=['blend_name']))
st.session_state.prediction_results = results_df.iloc[0].values
# --- Conditional cost calculation ---
# 5. Calculate cost only if all unit costs are provided and greater than zero
if all(c > 0.0 for c in unit_costs):
st.session_state.preopt_cost = sum(f * c for f, c in zip(fractions, unit_costs))
st.session_state.cost_calculated = True
else:
st.session_state.preopt_cost = 0.0
st.session_state.cost_calculated = False
# st.session_state.preopt_cost = sum(f * c for f, c in zip(fractions, unit_costs))
# 6. Store inputs for saving/downloading
st.session_state.last_input_data = model_input_data
st.session_state.prediction_made = True
st.success("Prediction complete!")
# def handle_prediction():
# """
# Gathers data from UI, formats it, runs prediction, and stores results.
# """
# start_time = time.time() # Start the timer
# log_activity("prediction", "User ran a new blend prediction.")
# fractions = []
# properties_by_comp = [[] for _ in range(5)]
# unit_costs = []
# # 1. Gather all inputs from session state
# for i in range(5):
# frac = st.session_state.get(f"c{i}_fraction", 0.0)
# fractions.append(frac)
# unit_costs.append(st.session_state.get(f"c{i}_cost", 0.0))
# for j in range(1, 11):
# prop = st.session_state.get(f"c{i}_prop{j}", 0.0)
# properties_by_comp[i].append(prop)
# # 2. Validate weights
# if abs(sum(fractions) - 1.0) > 0.01:
# st.warning("⚠️ Total of component fractions must sum to 1.0.")
# st.session_state.prediction_made = False
# return
# # 3. Format DataFrame for the model
# model_input_data = {"blend_name": [st.session_state.get("blend_name", "Untitled Blend")]}
# for i in range(5):
# model_input_data[f'Component{i+1}_fraction'] = [fractions[i]]
# for j in range(10):
# for i in range(5):
# col_name = f'Component{i+1}_Property{j+1}'
# model_input_data[col_name] = [properties_by_comp[i][j]]
# df_model = pd.DataFrame(model_input_data)
# # 4. Run prediction
# predictor = st.session_state.predictor
# results_df = predictor.predict_all(df_model.drop(columns=['blend_name']))
# st.session_state.prediction_results = results_df.iloc[0].values
# # 5. Calculate cost
# if all(c > 0.0 for c in unit_costs):
# st.session_state.preopt_cost = sum(f * c for f, c in zip(fractions, unit_costs))
# st.session_state.cost_calculated = True
# else:
# st.session_state.preopt_cost = 0.0
# st.session_state.cost_calculated = False
# # 6. Store inputs for saving/downloading
# st.session_state.last_input_data = model_input_data
# st.session_state.prediction_made = True
# # --- FIX: Stop the timer and create the new success message ---
# end_time = time.time()
# duration = end_time - start_time
# st.success(f"✅ Prediction complete in {duration:.2f} seconds! Scroll down to see the results.")
def handle_save_prediction():
"""Formats the last prediction's data and saves it to the database."""
if not st.session_state.get('prediction_made', False):
st.error("Please run a prediction before saving.")
return
# Prepare DataFrame in the format expected by `add_blends`
# save_df_data = st.session_state.last_input_data.copy()
# --- FIX: This gets the most recent blend name before saving ---
save_df_data = st.session_state.last_input_data.copy()
save_df_data['blend_name'] = [st.session_state.get('blend_name', 'Untitled Blend')]
# Add blend properties and cost
for i, prop_val in enumerate(st.session_state.prediction_results, 1):
save_df_data[f'BlendProperty{i}'] = [prop_val]
save_df_data['PreOpt_Cost'] = [st.session_state.preopt_cost]
# Add unit costs
for i in range(5):
save_df_data[f'Component{i+1}_unit_cost'] = st.session_state.get(f'c{i}_cost', 0.0)
save_df = pd.DataFrame(save_df_data)
try:
result = add_blends(save_df)
log_activity("save_prediction", f"Saved blend: {save_df['blend_name'].iloc[0]}")
get_all_blends_data.clear()
st.success(f"Successfully saved blend '{save_df['blend_name'].iloc[0]}' to the database!")
except Exception as e:
st.error(f"Failed to save blend: {e}")
# --- UI Rendering ---
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, key="batch_blend_mode")
# --- This is the new, fully functional batch mode block ---
if batch_blend:
st.subheader("📤 Batch Processing")
st.markdown("Upload a CSV file with blend recipes to predict their properties in bulk. The file must contain the 55 feature columns required by the model.")
# Provide a template for download
# NOTE: You will need to create a dummy CSV file named 'batch_template.csv'
# with the 55 required column headers for this to work.
try:
with open("assets/batch_template.csv", "rb") as f:
st.download_button(
label="📥 Download Batch Template (CSV)",
data=f,
file_name="batch_template.csv",
mime="text/csv"
)
except FileNotFoundError:
st.warning("Batch template file not found. Please create 'assets/batch_template.csv'.")
uploaded_file = st.file_uploader("Upload your CSV file", type=["csv"], key="batch_upload")
if uploaded_file is not None:
try:
input_df = pd.read_csv(uploaded_file)
st.markdown("##### Uploaded Data Preview")
st.dataframe(input_df.head())
if st.button("🧪 Run Batch Prediction", use_container_width=True, type="primary"):
# Basic validation: check for at least the fraction columns
required_cols = [f'Component{i+1}_fraction' for i in range(5)]
if not all(col in input_df.columns for col in required_cols):
st.error(f"Invalid file format. The uploaded CSV is missing one or more required columns like: {', '.join(required_cols)}")
else:
with st.spinner("Running batch prediction... This may take a moment."):
# Run prediction on the entire DataFrame
predictor = st.session_state.predictor
results_df = predictor.predict_all(input_df)
# Combine original data with the results
# Ensure column names for results are clear
results_df.columns = [f"BlendProperty{i+1}" for i in range(results_df.shape[1])]
# Combine input and output dataframes
final_df = pd.concat([input_df.reset_index(drop=True), results_df.reset_index(drop=True)], axis=1)
st.session_state['batch_results'] = final_df
st.success("Batch prediction complete!")
except Exception as e:
st.error(f"An error occurred while processing the file: {e}")
# Display results and download button if they exist in the session state
if 'batch_results' in st.session_state:
st.markdown("---")
st.subheader("✅ Batch Prediction Results")
results_to_show = st.session_state['batch_results']
st.dataframe(results_to_show)
csv_data = results_to_show.to_csv(index=False).encode('utf-8')
st.download_button(
label="📥 Download Full Results (CSV)",
data=csv_data,
file_name="batch_prediction_results.csv",
mime="text/csv",
use_container_width=True
)
else:
# --- Manual Blend Designer UI ---
all_components_df = get_components_from_db()
# st.text_input("Blend Name", "My New Blend", key="blend_name", help="Give your blend a unique name before saving.")
# st.markdown("---")
for i in range(5):
# Unique keys for each widget within the component expander
select_key = f"c{i}_select"
name_key = f"c{i}_name"
frac_key = f"c{i}_fraction"
cost_key = f"c{i}_cost"
# Check if a selection from dropdown was made
if select_key in st.session_state and st.session_state[select_key] != "---":
selected_name = st.session_state[select_key]
comp_data = all_components_df[all_components_df['component_name'] == selected_name].iloc[0]
# Auto-populate session state values
st.session_state[name_key] = comp_data['component_name']
st.session_state[frac_key] = comp_data.get('component_fraction', 0.2)
# st.session_state[cost_key] = comp_data.get('unit_cost', 0.0)
# --- Handle missing unit_cost from DB correctly ---
cost_val = comp_data.get('unit_cost', 0.0)
st.session_state[cost_key] = 0.0 if pd.isna(cost_val) else float(cost_val)
for j in range(1, 11):
prop_key = f"c{i}_prop{j}"
st.session_state[prop_key] = comp_data.get(f'property{j}', 0.0)
# Reset selectbox to avoid re-triggering
st.session_state[select_key] = "---"
with st.expander(f"**Component {i+1}**", expanded=(i==0)):
# --- This is the placeholder for your custom filter ---
# Example: Only show components ending with a specific number
# filter_condition = all_components_df['component_name'].str.endswith(str(i + 1))
# For now, we show all components
filter_condition = pd.Series([True] * len(all_components_df), index=all_components_df.index)
filtered_df = all_components_df[filter_condition]
#component_options = ["---"] + filtered_df['component_name'].tolist()
# component_options = ["---"] + [m for m in filtered_df['component_name'].tolist() if m.endswith(f"Component_{i+1}") ]
options = filter_component_options(all_components_df, i)
component_options = ["---"] + options
st.selectbox(
"Load from Registry",
options=component_options,
key=select_key,
help="Select a saved component to auto-populate its properties."
)
c1, c2, c3 = st.columns([1.5, 2, 2])
with c1:
st.text_input("Component Name", key=name_key)
st.number_input("Fraction", min_value=0.0, max_value=1.0, step=0.01, key=frac_key, format="%.3f")
st.number_input("Unit Cost ($)", min_value=0.0, step=0.01, key=cost_key, format="%.2f")
with c2:
for j in range(1, 6):
st.number_input(f"Property {j}", key=f"c{i}_prop{j}", format="%.4f")
with c3:
for j in range(6, 11):
st.number_input(f"Property {j}", key=f"c{i}_prop{j}", format="%.4f")
st.markdown('<div style="height:10px;"></div>', unsafe_allow_html=True)
# st.button("🧪 Predict Blended Properties", on_click=handle_prediction, use_container_width=True, type="primary")
# --- FIX: Changed button call to prevent page jumping ---
# --- In the "Manual Blend Designer UI" section ---
if st.button("🧪 Predict Blended Properties", use_container_width=False, type="primary"):
with st.spinner("🧠 Running prediction... Please wait."):
handle_prediction()
# --- Results Section ---
if st.session_state.get('prediction_made', False):
st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
st.subheader("📈 Prediction Results")
results_array = st.session_state.get('prediction_results', np.zeros(10))
# Display the 10 Property KPI cards
kpi_cols = st.columns(5)
for i in range(10):
with kpi_cols[i % 5]:
st.markdown(f"""
<div class="metric-card" style="margin-bottom: 10px; padding: 0.8rem;">
<div class="metric-label" style="font-size: 0.8rem;">Blend Property {i+1}</div>
<div class="metric-value" style="font-size: 1.5rem;">{results_array[i]:.4f}</div>
</div>
""", unsafe_allow_html=True)
# Display the Centered, smaller cost KPI card
_, mid_col, _ = st.columns([1.5, 2, 1.5])
with mid_col:
cost_val = st.session_state.get('preopt_cost', 0.0)
cost_calculated = st.session_state.get('cost_calculated', False)
if cost_calculated:
cost_display = f"${cost_val:,.2f}"
delta_text = "Per unit fuel"
else:
cost_display = "N/A"
delta_text = "Enter all component costs to calculate"
st.markdown(f"""
<div class="metric-card" style="border-color: #8B4513; background: #FFF8E1; padding: 0.8rem;">
<div class="metric-label" style="font-size: 0.8rem;">Predicted Blend Cost</div>
<div class="metric-value" style="font-size: 1.5rem;">{cost_display}</div>
<div class="metric-delta" style="font-size: 0.8rem;">{delta_text}</div>
</div>
""", unsafe_allow_html=True)
# --- Visualizations & Actions Section ---
st.subheader("📊 Visualizations & Actions")
vis_col1, vis_col2 = st.columns(2)
with vis_col1:
# Pie Chart
fractions = [st.session_state.get(f"c{i}_fraction", 0.0) for i in range(5)]
labels = [st.session_state.get(f"c{i}_name", f"Component {i+1}") for i in range(5)]
pie_fig = px.pie(
values=fractions, names=labels, title="Component Fractions",
hole=0.4, color_discrete_sequence=px.colors.sequential.YlOrBr_r
)
pie_fig.update_traces(textposition='inside', textinfo='percent+label')
st.plotly_chart(pie_fig, use_container_width=True)
# --- This is the ONE AND ONLY 'blend_name' input ---
st.text_input(
"Blend Name for Saving",
"My New Blend",
key="blend_name",
help="Give your blend a unique name before saving."
)
with vis_col2:
# Bar Chart
prop_to_view = st.selectbox(
"Select Property to Visualize",
options=[f"Property{j}" for j in range(1, 11)],
key="viz_property_select"
)
prop_idx = int(prop_to_view.replace("Property", "")) - 1
bar_values = [st.session_state.get(f"c{i}_prop{prop_idx+1}", 0.0) for i in range(5)]
blend_prop_value = results_array[prop_idx]
bar_labels = [f"Comp {i+1}" for i in range(5)] + ["Blend"]
all_values = bar_values + [blend_prop_value]
bar_df = pd.DataFrame({"Component": bar_labels, "Value": all_values})
# --- Lighter brown color for the bars ---
bar_colors = ['#A67C52'] * 5 + ['#654321']
bar_fig = px.bar(bar_df, x="Component", y="Value", title=f"Comparison for {prop_to_view}")
bar_fig.update_traces(marker_color=bar_colors)
bar_fig.update_layout(showlegend=False)
st.plotly_chart(bar_fig, use_container_width=True)
# Download button is aligned here
download_df = pd.DataFrame(st.session_state.last_input_data)
file_name = st.session_state.get('blend_name', 'blend_results').replace(' ', '_')
for i in range(5):
download_df[f'Component{i+1}_unit_cost'] = st.session_state.get(f'c{i}_cost', 0.0)
for i, res in enumerate(results_array, 1):
download_df[f'BlendProperty{i}'] = res
csv_data = download_df.to_csv(index=False).encode('utf-8')
st.download_button(
label="📥 Download Results as CSV",
data=csv_data,
file_name=f"{file_name}.csv",
mime='text/csv',
use_container_width=True,
help="Download all inputs and predicted outputs to a CSV file."
)
# --- This is the ONE AND ONLY 'Save' button ---
if st.button("💾 Save Prediction to Database", use_container_width=False):
handle_save_prediction()
# This empty markdown is a trick to add vertical space
st.markdown('<div style="height: 36px;"></div>', unsafe_allow_html=True)
# --- Floating "How to Use" button ---
st.markdown("""
<style>
/* Styles for the help panel and button */
#help-toggle-designer{display:none;}
.help-button-designer{
position:fixed; right:25px; bottom:25px; z-index:9999;
background:#8B4513; color:#FFD700; padding:16px 22px; font-size:17px;
border-radius:18px; font-weight:900; box-shadow:0 8px 22px rgba(0,0,0,0.2);
cursor:pointer; border:0;
}
.help-panel-designer{
position:fixed; right:25px; bottom:100px; z-index:9998;
width:520px; max-height:70vh; overflow-y:auto;
background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
border:1px solid #CFB53B; border-radius:12px; padding:20px; box-shadow:0 14px 34px rgba(0,0,0,0.22);
color:#4a2f1f; transform: translateY(12px); opacity:0; visibility:hidden; transition: all .22s ease-in-out;
}
#help-toggle-designer:checked + label.help-button-designer + .help-panel-designer{
opacity:1; visibility:visible; transform: translateY(0);
}
/* Style for the header and close button inside the panel */
.help-panel-designer .head{display:flex; justify-content:space-between; align-items:center; margin-bottom:12px}
.help-panel-designer .title{font-weight:900; color:#654321; font-size:16px}
.help-panel-designer .help-close{background:#8B4513; color:#FFD700; padding:6px 10px; border-radius:8px; cursor:pointer; font-weight:800}
</style>
<input id="help-toggle-designer" type="checkbox" />
<label for="help-toggle-designer" class="help-button-designer">💬 Help</label>
<div class="help-panel-designer">
<div class="head">
<div class="title">Using the Blend Designer</div>
<label for="help-toggle-designer" class="help-close">Close</label>
</div>
<p style="margin-top:0;">This tab is your creative sandbox for designing and predicting fuel properties. It has two modes:</p>
<b style="color: #654321;">Manual Mode (Default):</b>
<ul style="padding-left: 20px; list-style-position: outside; margin-bottom:0;">
<li style="margin-bottom: 8px;"><b>Configure:</b> Define up to five components. Use the 'Load from Registry' dropdown to auto-fill data or enter properties manually.</li>
<li style="margin-bottom: 8px;"><b>Predict:</b> Once component fractions sum to 1.0, click <b>Predict</b>. The AI calculates the blend's 10 properties and its cost.</li>
<li style="margin-bottom: 8px;"><b>Analyze:</b> Two charts appear after prediction. The <b>Pie Chart</b> shows the component mix. The <b>Bar Chart</b> compares each component's property to the final blend's.</li>
<li style="margin-bottom: 8px;"><b>Save:</b> After predicting, enter a unique name and save the blend to the database.</li>
</ul>
<p style="margin-top:15px;"><b style="color: #654321;">Batch Blend Mode:</b></p>
<ul style="padding-left: 20px; list-style-position: outside; margin-top:0;">
<li style="margin-bottom: 8px;"><b>Activate:</b> Toggle on Batch Mode to predict many recipes at once.</li>
<li style="margin-bottom: 8px;"><b>Process:</b> Download the CSV template, fill it with your data, upload it, and click 'Run Batch Prediction'.</li>
<li style="margin-bottom: 8px;"><b>Download:</b> The results for all your blends will appear in a table, ready to download.</li>
</ul>
</div>
""", unsafe_allow_html=True)
## ----------------------------------------------------------------------------------------------------------------------------------------------
## Optimization Engine Tab
##-----------------------------------------------------------------------------------------------------------------------------------------------
with tabs[2]:
st.subheader("⚙️ Optimization Engine")
st.markdown("Define your property goals, select base components, and run the optimizer to find the ideal blend recipe.")
# --- State Initialization ---
if 'optimization_running' not in st.session_state:
st.session_state.optimization_running = False
if 'optimization_results' not in st.session_state:
st.session_state.optimization_results = None
if 'optimization_time' not in st.session_state:
st.session_state.optimization_time = 0.0
# --- Optimization Goals ---
st.markdown("#### 1. Define Optimization Goals")
# Using a container to group the goal inputs
with st.container(border=True):
cols_row1 = st.columns(5)
cols_row2 = st.columns(5)
for i in range(1, 11):
col = cols_row1[(i-1)] if i <= 5 else cols_row2[(i-6)]
with col:
st.number_input(f"Property {i}", key=f"opt_target_{i}", value=0.0, step=0.01, format="%.4f")
st.toggle("Fix Target", key=f"opt_fix_{i}", help=f"Toggle on to make Property {i} a fixed constraint.")
# --- Component Selection (Copied and Adapted) ---
st.markdown("#### 2. Select Initial Components")
all_components_df_opt = get_components_from_db() # Use a different variable to avoid conflicts
main_cols = st.columns(2)
with main_cols[0]: # Left side for first 3 components
for i in range(3):
with st.expander(f"**Component {i+1}**", expanded=(i==0)):
# Auto-population and input fields logic (reused from Blend Designer)
# Note: Keys are prefixed with 'opt_' to ensure they are unique to this tab
select_key, name_key, frac_key, cost_key = f"opt_c{i}_select", f"opt_c{i}_name", f"opt_c{i}_fraction", f"opt_c{i}_cost"
# Auto-population logic...
if select_key in st.session_state and st.session_state[select_key] != "---":
selected_name = st.session_state[select_key]
comp_data = all_components_df_opt[all_components_df_opt['component_name'] == selected_name].iloc[0]
st.session_state[name_key] = comp_data['component_name']
st.session_state[frac_key] = comp_data.get('component_fraction', 0.2)
cost_val = comp_data.get('unit_cost', 0.0)
st.session_state[cost_key] = 0.0 if pd.isna(cost_val) else float(cost_val)
for j in range(1, 11):
st.session_state[f"opt_c{i}_prop{j}"] = comp_data.get(f'property{j}', 0.0)
st.session_state[select_key] = "---"
# UI for component
# component_options = ["---"] + all_components_df_opt['component_name'].tolist()
options = filter_component_options(all_components_df_opt, i)
component_options = ["---"] + options
st.selectbox("Load from Registry", options=component_options, key=select_key)
c1, c2, c3 = st.columns([1.5, 2, 2])
with c1:
st.text_input("Component Name", key=name_key)
st.number_input("Unit Cost ($)", min_value=0.0, step=0.01, key=cost_key, format="%.2f")
with c2:
for j in range(1, 6): st.number_input(f"Property {j}", key=f"opt_c{i}_prop{j}", format="%.4f")
with c3:
for j in range(6, 11): st.number_input(f"Property {j}", key=f"opt_c{i}_prop{j}", format="%.4f")
with main_cols[1]: # Right side for last 2 components and controls
for i in range(3, 5):
with st.expander(f"**Component {i+1}**", expanded=False):
# Auto-population and input fields logic...
select_key, name_key, frac_key, cost_key = f"opt_c{i}_select", f"opt_c{i}_name", f"opt_c{i}_fraction", f"opt_c{i}_cost"
if select_key in st.session_state and st.session_state[select_key] != "---":
selected_name = st.session_state[select_key]
comp_data = all_components_df_opt[all_components_df_opt['component_name'] == selected_name].iloc[0]
st.session_state[name_key] = comp_data['component_name']
st.session_state[frac_key] = comp_data.get('component_fraction', 0.2)
cost_val = comp_data.get('unit_cost', 0.0)
st.session_state[cost_key] = 0.0 if pd.isna(cost_val) else float(cost_val)
for j in range(1, 11):
st.session_state[f"opt_c{i}_prop{j}"] = comp_data.get(f'property{j}', 0.0)
st.session_state[select_key] = "---"
# component_options = ["---"] + all_components_df_opt['component_name'].tolist()
options = filter_component_options(all_components_df_opt, i)
component_options = ["---"] + options
st.selectbox("Load from Registry", options=component_options, key=select_key)
c1, c2, c3 = st.columns([1.5, 2, 2])
with c1:
st.text_input("Component Name", key=name_key)
st.number_input("Unit Cost ($)", min_value=0.0, step=0.01, key=cost_key, format="%.2f")
with c2:
for j in range(1, 6): st.number_input(f"Property {j}", key=f"opt_c{i}_prop{j}", format="%.4f")
with c3:
for j in range(6, 11): st.number_input(f"Property {j}", key=f"opt_c{i}_prop{j}", format="%.4f")
# --- Optimization Controls ---
with st.container(border=True):
st.markdown("##### 3. Configure & Run")
st.checkbox("Include Cost in Optimization", value=True, key="opt_include_cost")
# ... inside the "Configure & Run" container ...
st.slider(
"Optimization Steps (Generations)",
min_value=10, max_value=100, value=20, key="opt_generations",
help="Controls how many iterations the algorithm runs. Higher is slower but finds better solutions."
)
st.slider(
"Optimization Depth (Population Size)",
min_value=10, max_value=500, value=10, key="opt_pop_size",
help="Controls how many candidate solutions are tested in each step. Higher is slower but explores more options."
)
run_button_col, spinner_col = st.columns([3, 1])
# ... rest of the container code ...
# Run button and spinner logic
run_button_col, spinner_col = st.columns([3, 1])
with run_button_col:
# New Code:
if st.button("🚀 Run Optimization", use_container_width=False, type="primary", disabled=st.session_state.optimization_running):
st.session_state.optimization_running = True
log_activity("optimization")
start_time = time.time()
# --- FIX: Create a placeholder for the progress bar ---
progress_placeholder = st.empty()
# Gather data for the optimization function
targets = {f"Property{i}": st.session_state.get(f"opt_target_{i}", 0.0) for i in range(1, 11)}
fixed_targets = {f"Property{i}": targets[f"Property{i}"] for i in range(1, 11) if st.session_state.get(f"opt_fix_{i}", False)}
include_cost = st.session_state.get('opt_include_cost', True)
generations = st.session_state.get('opt_generations', 20)
pop_size = st.session_state.get('opt_pop_size', 20)
# Initialize the progress bar in the placeholder
progress_bar = progress_placeholder.progress(0, text="Initializing Optimization...")
# Call the function, passing the progress_bar object
st.session_state.optimization_results = run_real_optimization(
targets, fixed_targets, None, include_cost, generations, pop_size, progress_bar
)
# Update the bar to 100% and show a completion message
progress_bar.progress(1.0, text="Optimization Complete!")
time.sleep(1.5) # Optional: pause for a moment to show completion
progress_placeholder.empty() # Clear the progress bar from the screen
st.session_state.optimization_time = time.time() - start_time
st.session_state.optimization_running = False
st.rerun()
with spinner_col:
if st.session_state.optimization_running:
st.markdown('<div class="spinner"></div>', unsafe_allow_html=True)
if st.session_state.optimization_time > 0:
st.success(f"Optimization complete in {st.session_state.optimization_time:.2f} seconds. Scroll down to see Results")
# --- Results Section ---
if st.session_state.optimization_results:
st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
st.subheader("🏆 Optimization Results")
results = st.session_state.optimization_results
# --- FIX: Add sorting controls ---
st.markdown("##### Sort Solutions By")
sort_option = st.radio(
"Sort Solutions By",
options=["Best Quality (Lowest Error)", "Lowest Cost", "Best Quality Score"],
horizontal=True,
label_visibility="collapsed"
)
# Dynamically sort the results list based on the selected option
if sort_option == "Lowest Cost":
sorted_results = sorted(results, key=lambda x: x.get('optimized_cost', float('inf')))
elif sort_option == "Best Quality Score":
# Calculate score for each result before sorting
for res in results:
res['quality_score'] = calculate_quality_score(res.get("error"))
sorted_results = sorted(results, key=lambda x: x.get('quality_score', 0), reverse=True)
else: # Default sort by error
sorted_results = results # Already pre-sorted by the function
# --- FIX: Populate dropdown with all sorted results ---
# The first item in the list is now always the "best" according to the sort
result_options = {
i: f"Solution {i+1} (Error: {res['error']:.4f}, Cost: ${res.get('optimized_cost', 0):.2f})"
for i, res in enumerate(sorted_results)
}
st.markdown("##### Select Solution to View")
selected_idx = st.selectbox(
"Select Solution to View",
options=list(result_options.keys()),
format_func=lambda x: result_options[x],
label_visibility="collapsed"
)
# The rest of the UI will automatically update based on the selected solution
selected_solution = sorted_results[selected_idx]
# --- New Layout for Component Fractions (Centered) ---
st.markdown("##### Optimal Component Fractions")
_, c1, c2, c3, c4, c5, _ = st.columns([0.5, 1, 1, 1, 1, 1, 0.5])
cols = [c1, c2, c3, c4, c5]
for i, frac in enumerate(selected_solution["component_fractions"]):
with cols[i]:
comp_name = st.session_state.get(f"opt_c{i}_name") or f"Component {i+1}"
st.markdown(f"""
<div class="metric-card" style="padding: 0.8rem;">
<div class="metric-label" style="font-size: 0.8rem;">{comp_name}</div>
<div class="metric-value" style="font-size: 1.5rem;">{frac*100:.2f}%</div>
</div>
""", unsafe_allow_html=True)
st.markdown('<div style="height:15px;"></div>', unsafe_allow_html=True) # Spacer
# --- New Layout for 10 Blend Properties (Full Width) ---
st.markdown("##### Resulting Blend Properties")
prop_kpi_cols = st.columns(10)
for i, prop_val in enumerate(selected_solution["blend_properties"]):
with prop_kpi_cols[i]:
st.markdown(f"""
<div class="metric-card" style="margin-bottom: 10px; padding: 0.5rem;">
<div class="metric-label" style="font-size: 0.7rem;">Property {i+1}</div>
<div class="metric-value" style="font-size: 1.1rem;">{prop_val:.4f}</div>
</div>
""", unsafe_allow_html=True)
# --- REPLACEMENT FOR THE "Cost Analysis" SECTION ---
st.markdown("##### Performance Analysis")
# Calculate baseline cost and quality score
component_costs = [st.session_state.get(f"opt_c{i}_cost", 0.0) for i in range(5)]
baseline_cost = sum(0.2 * cost for cost in component_costs)
optimized_cost = selected_solution.get("optimized_cost", 0.0)
quality_score = calculate_quality_score(selected_solution.get("error"))
# Use more columns to make the cards smaller
_, c1, c2, c3, c4, _ = st.columns([0.5, 1.5, 1.5, 1.5, 1.5, 0.5])
with c1:
st.markdown(f"""
<div class="metric-card">
<div class="metric-label">Baseline Cost</div>
<div class="metric-value">${baseline_cost:.2f}</div>
</div>
""", unsafe_allow_html=True)
with c2:
st.markdown(f"""
<div class="metric-card">
<div class="metric-label">Optimized Cost</div>
<div class="metric-value">${optimized_cost:.2f}</div>
</div>
""", unsafe_allow_html=True)
with c3:
savings = baseline_cost - optimized_cost
savings_color = "green" if savings >= 0 else "red"
st.markdown(f"""
<div class="metric-card">
<div class="metric-label">Savings</div>
<div class="metric-value" style="color:{savings_color};">${savings:.2f}</div>
</div>
""", unsafe_allow_html=True)
with c4:
st.markdown(f"""
<div class="metric-card" style="border-color: #8B4513; background: #FFF8E1;">
<div class="metric-label">Quality Score</div>
<div class="metric-value">{quality_score:.1f}</div>
</div>
""", unsafe_allow_html=True)
st.markdown('<div style="height:30px;"></div>', unsafe_allow_html=True) # Spacer
# Expander for full results table
with st.expander("Show Full Results Table"):
table_data = []
for i in range(5):
row = {
"Composition": st.session_state.get(f"opt_c{i}_name", f"C{i+1}"),
"Fraction": selected_solution["component_fractions"][i],
"Unit Cost": st.session_state.get(f"opt_c{i}_cost", 0.0)
}
for j in range(1, 11):
row[f"Property {j}"] = st.session_state.get(f"opt_c{i}_prop{j}", 0.0)
table_data.append(row)
# Add blend row
blend_row = {"Composition": "Optimized Blend", "Fraction": 1.0, "Unit Cost": selected_solution["optimized_cost"]}
for i, prop in enumerate(selected_solution["blend_properties"]):
blend_row[f"Property {i+1}"] = prop
table_data.append(blend_row)
st.dataframe(pd.DataFrame(table_data), use_container_width=True)
# Pareto Plot and Save Section
pareto_col, save_col = st.columns([2, 1])
with pareto_col:
st.markdown("##### Pareto Front: Cost vs. Error")
pareto_df = pd.DataFrame({
'Cost': [r['optimized_cost'] for r in results],
'Error': [r['error'] for r in results],
'Solution': [f'Sol {i+1}' for i in range(len(results))]
})
# --- FIX: Inverted the axes to show Error vs. Cost ---
fig_pareto = px.scatter(
pareto_df, x='Error', y='Cost', text='Solution', title="<b>Pareto Front: Error vs. Cost</b>"
)
fig_pareto.update_traces(textposition='top center', marker=dict(size=12, color='#8B4513'))
st.plotly_chart(fig_pareto, use_container_width=True)
with save_col:
st.markdown("##### Save Result")
st.text_input("Save as Blend Name", value=f"Optimized_Blend_{selected_idx+1}", key="opt_save_name")
# --- REPLACEMENT FOR THE SAVE BUTTON LOGIC ---
if st.button("💾 Save to Database", use_container_width=False):
# Prepare DataFrame in the format expected by `add_blends`
save_data = {}
# 1. Add blend name
save_name = st.session_state.get("opt_save_name", f"Optimized_Blend_{selected_idx+1}")
save_data['blend_name'] = [save_name]
# 2. Add component fractions and costs from UI
for i in range(5):
save_data[f'Component{i+1}_fraction'] = selected_solution["component_fractions"][i]
for j in range(1, 11):
save_data[f'Component{i+1}_Property{j}'] = st.session_state.get(f"opt_c{i}_prop{j}", 0.0)
save_data[f'Component{i+1}_unit_cost'] = st.session_state.get(f"opt_c{i}_cost", 0.0)
# 3. Add the 10 final blend properties
for i, prop_val in enumerate(selected_solution["blend_properties"], 1):
save_data[f'BlendProperty{i}'] = prop_val
# 4. Add the PreOpt (Baseline) and Optimized costs
component_costs = [st.session_state.get(f"opt_c{i}_cost", 0.0) for i in range(5)]
baseline_cost = sum(0.2 * cost for cost in component_costs)
optimized_cost = selected_solution.get("optimized_cost", 0.0)
save_data['PreOpt_Cost'] = [baseline_cost]
save_data['Optimized_Cost'] = [optimized_cost]
save_data['Quality_Score'] = [calculate_quality_score(selected_solution.get("error"))]
save_df = pd.DataFrame(save_data)
try:
result = add_blends(save_df)
log_activity("save_optimization", f"Saved optimized blend: {save_name}")
st.success(f"Successfully saved blend '{save_name}' to the database!")
get_all_blends_data.clear() # Clear cache for comparison tab
except Exception as e:
st.error(f"Failed to save blend: {e}")
# Placeholder for download button logic
st.download_button("📥 Download All Solutions (CSV)", data="dummy_csv_data", file_name="optimization_results.csv", use_container_width=False)
# --- Floating Help Button ---
# (Using a different key to avoid conflict with other tabs)
# --- FIX: Complete working version of the help button ---
st.markdown("""
<style>
#help-toggle-optimizer { display: none; }
#help-toggle-optimizer:checked ~ .help-panel-optimizer {
opacity: 1; visibility: visible; transform: translateY(0);
}
.help-panel-optimizer {
position:fixed; right:25px; bottom:100px; z-index:9998;
width:520px; max-height:70vh; overflow-y:auto;
background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
border:1px solid #CFB53B; border-radius:12px; padding:20px;
box-shadow:0 14px 34px rgba(0,0,0,0.22);
color:#4a2f1f; transform: translateY(12px); opacity:0;
visibility:hidden; transition: all .22s ease-in-out;
}
</style>
<input id="help-toggle-optimizer" type="checkbox" />
<label for="help-toggle-optimizer" class="help-button">💬 Help</label>
<div class="help-panel help-panel-optimizer"> <div class="head">
<div class="title">How to Use the Optimizer</div>
<label for="help-toggle-optimizer" class="help-close">Close</label>
</div>
<div class="help-body">
<p><b>1. Define Goals:</b> Enter your desired target values for each of the 10 blend properties. Use the 'Fix Target' toggle for any property that must be met exactly.</p>
<p><b>2. Select Components:</b> Choose up to 5 base components. You can load them from the registry to auto-fill their data or enter them manually.</p>
<p><b>3. Configure & Run:</b> Decide if cost should be a factor in the optimization, then click 'Run Optimization'. A spinner will appear while the process runs.</p>
<p><b>4. Analyze Results:</b> After completion, the best solution is shown by default. You can view other potential solutions from the dropdown. The results include optimal component fractions and the final blend properties.</p>
<p><b>5. Save & Download:</b> Give your chosen solution a name and save it to the blends database for future use in the Comparison tab.</p>
</div>
</div>
""", unsafe_allow_html=True)
## -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
## Blend Comparison Tab
## -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
@st.cache_data
def get_blend_property_ranges(db_path="eagleblend.db") -> dict:
"""Calculates the min and max for each BlendProperty across all blends."""
ranges = {}
with sqlite3.connect(db_path) as conn:
for i in range(1, 11):
prop_name = f"BlendProperty{i}"
query = f"SELECT MIN({prop_name}), MAX({prop_name}) FROM blends WHERE {prop_name} IS NOT NULL"
min_val, max_val = conn.execute(query).fetchone()
ranges[prop_name] = (min_val if min_val is not None else 0, max_val if max_val is not None else 1)
return ranges
with tabs[3]:
st.subheader("📊 Blend Scenario Comparison")
# --- Initial Data Loading ---
all_blends_df = get_all_blends_data()
property_ranges = get_blend_property_ranges()
if all_blends_df.empty:
st.warning("No blends found in the database. Please add blends in the 'Fuel Registry' tab to use this feature.")
else:
# --- Scenario Selection ---
st.markdown("Select up to three blends from the registry to compare their properties and performance.")
cols = st.columns(3)
selected_blends = []
blend_names = all_blends_df['blend_name'].tolist()
for i, col in enumerate(cols):
with col:
choice = st.selectbox(
f"Select Blend for Scenario {i+1}",
options=["-"] + blend_names,
key=f"blend_select_{i}"
)
if choice != "-":
selected_blends.append(choice)
# Filter the main dataframe to only include selected blends
if selected_blends:
#--- FIX: Filter duplicates to get only the most recent entry for each blend name ---
filtered_df = all_blends_df[all_blends_df['blend_name'].isin(selected_blends)]
comparison_df = filtered_df.sort_values('id', ascending=False).drop_duplicates(subset=['blend_name']).set_index('blend_name')
# --- Information Cards ---
st.markdown("---")
# --- FIX: This new block creates a stable 3-column layout ---
st.markdown("#### Selected Blend Overview")
card_cols = st.columns(3) # Create a fixed 3-column layout immediately
for i, blend_name in enumerate(selected_blends):
# Place each selected blend into its corresponding column
with card_cols[i]:
blend_data = comparison_df.loc[blend_name]
#--- FIX: Use pd.isna() for a robust check of the timestamp value ---
created_val = blend_data.get('created_at')
created_at = pd.to_datetime(created_val).strftime('%Y-%m-%d') if not pd.isna(created_val) else 'N/A'
# Component Fractions
fractions_html = ""
for j in range(1, 6):
frac = blend_data.get(f"Component{j}_fraction", 0) * 100
if frac > 0:
fractions_html += f"<span style='font-weight: 500;'>C{j}:</span> {frac:.1f}% &nbsp; "
# Blend Properties
properties_html = ""
for j in range(1, 11):
prop = blend_data.get(f"BlendProperty{j}")
if prop is not None:
properties_html += f"<span style='background: #f0f2f6; padding: 2px 5px; border-radius: 4px; margin-right: 4px; font-size: 0.8rem;'><b>P<sub>{j}</sub>:</b> {prop:.3f}</span>"
st.markdown(f"""
<div class="metric-card" style="padding: 1rem; height: 100%;">
<div class="metric-label" style="font-size: 1.1rem; text-align: left;">{blend_name}</div>
<div style="font-size: 0.8rem; text-align: left; color: #6c757d; margin-bottom: 10px;">Created: {created_at}</div>
<div style="font-size: 0.9rem; text-align: left; margin-bottom: 10px;">{fractions_html}</div>
<div style="text-align: left;">{properties_html}</div>
</div>
""", unsafe_allow_html=True)
# --- Charting Section ---
st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
st.subheader("📈 Comparative Analysis")
plot_cols = st.columns(2)
with plot_cols[0]:
# --- Plot 1: Sorted Bar Plot (Cost) ---
# 1. Prepare data and sort it for clear visualization
costs_data = []
for name in selected_blends:
row = comparison_df.loc[name]
# Prioritize Optimized_Cost, then fall back to PreOpt_Cost
cost = row.get('Optimized_Cost') or row.get('PreOpt_Cost') or 0
if cost > 0: # Only include blends with a valid cost
costs_data.append({'Blend': name, 'Cost': cost})
if costs_data:
cost_df = pd.DataFrame(costs_data)
cost_df = cost_df.sort_values(by='Cost', ascending=False)
# 2. Create the horizontal bar plot with Plotly Express
fig_cost = px.bar(
cost_df,
x='Cost',
y='Blend',
orientation='h',
text='Cost',
title="<b>Blend Cost Comparison</b>",
labels={'Cost': 'Cost ($ per unit)', 'Blend': ''} # Use an empty string to remove the y-axis title
)
# 3. Apply professional styling
fig_cost.update_traces(
marker_color='#8B4513', # Use a theme-consistent dark brown
marker_line_color='#4a2f1f',
marker_line_width=1.5,
texttemplate='$%{text:,.2f}', # Format text as currency
textposition='outside',
insidetextfont=dict(color='white')
)
fig_cost.update_layout(
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
font=dict(color="#4a2f1f"),
uniformtext_minsize=8,
uniformtext_mode='hide'
)
st.plotly_chart(fig_cost, use_container_width=True)
else:
st.info("No cost data available for the selected blends to generate a comparison chart.")
# --- This is the new, more robust radar chart block ---
with plot_cols[1]:
# --- Plot 2: Radar Chart (Blend Properties) ---
categories = [f'P{i}' for i in range(1, 11)]
radar_data_exists = False
fig_radar = go.Figure()
for name in selected_blends:
values = [comparison_df.loc[name].get(f'BlendProperty{i}', 0) for i in range(1, 11)]
# Check if there's any non-zero data to plot
if any(v > 0 for v in values):
radar_data_exists = True
fig_radar.add_trace(go.Scatterpolar(
r=values, theta=categories, fill='toself', name=name
))
# Only show the chart if there is data, otherwise show a warning
if radar_data_exists:
fig_radar.update_layout(
title="<b>Blend Property Profile</b>",
polar=dict(radialaxis=dict(visible=True)),
showlegend=True,
height=500,
margin=dict(l=80, r=80, t=100, b=80),
legend=dict(orientation="h", yanchor="bottom", y=-0.2, xanchor="center", x=0.5)
)
st.plotly_chart(fig_radar, use_container_width=True)
else:
st.warning("Radar Chart cannot be displayed. The selected blend(s) have no property data in the database.", icon="📊")
# --- Plot 3 & 4 ---
plot_cols2 = st.columns(2)
with plot_cols2[0]:
# --- Plot 3: Scatter Plot (Cost vs Quality) ---
# 1. Prepare a self-contained DataFrame for this specific plot
scatter_data = []
for name in selected_blends:
row = comparison_df.loc[name]
cost = row.get('Optimized_Cost') or row.get('PreOpt_Cost') or 0
quality = row.get('Quality_Score', 0)
# Only include points that have valid data for both axes
if cost > 0 and quality > 0:
scatter_data.append({
'Blend': name,
'Cost': cost,
'Quality Score': quality
})
# 2. Create the plot only if there is data to show
if scatter_data:
scatter_df = pd.DataFrame(scatter_data)
fig_scatter = px.scatter(
scatter_df,
x='Cost',
y='Quality Score',
text='Blend',
labels={'Cost': 'Cost ($)', 'Quality Score': 'Quality Score'},
title="<b>Cost vs. Quality Frontier</b>"
)
fig_scatter.update_traces(
textposition='top center',
marker=dict(size=25, color='#8B4513', symbol='circle')
)
st.plotly_chart(fig_scatter, use_container_width=True)
else:
st.info("Not enough cost and quality data to generate the Cost vs. Quality plot.")
with plot_cols2[1]:
# --- Plot 4: 100% Stacked Bar (Component Fractions) ---
frac_data = comparison_df[[f'Component{i}_fraction' for i in range(1, 6)]].reset_index()
frac_data_melted = frac_data.melt(id_vars='blend_name', var_name='Component', value_name='Fraction')
fig_stacked = px.bar(
frac_data_melted, x='blend_name', y='Fraction', color='Component',
title="<b>Component Composition by Scenario</b>",
labels={'blend_name': 'Scenario'},
# --- FIX: Using a theme-consistent Yellow-Orange-Brown palette ---
# color_discrete_sequence=px.colors.sequential.YlOrBr_
# # --- FIX: Using Plotly's default palette for distinct colors (blue, red, green, etc.) ---
color_discrete_sequence=px.colors.qualitative.Plotly
# --- FIX: Using a qualitative palette for more distinct colors ---
# color_discrete_sequence=px.colors.qualitative.Vivid
)
fig_stacked.update_layout(barmode='stack')
st.plotly_chart(fig_stacked, use_container_width=True)
# --- Plot 5: Composite Bar Chart ---
st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
# --- FIX: Constrain selectbox width using columns ---
s_col1, s_col2, s_col3 = st.columns([1, 2, 1])
with s_col2:
prop_idx = st.selectbox(
"Select Property to Visualize (Pj)",
options=list(range(1, 11)),
format_func=lambda x: f"Property {x}",
key="composite_prop_select",
label_visibility="collapsed" # Hides the label to make it cleaner
)
comp_prop_name = f'Component{{}}_Property{prop_idx}'
blend_prop_name = f'BlendProperty{prop_idx}'
chart_data = []
for name in selected_blends:
for i in range(1, 6): # Components C1-C5
chart_data.append({
'Scenario': name,
'Composition': f'C{i}',
'Value': comparison_df.loc[name].get(comp_prop_name.format(i), 0)
})
# Blend Property
chart_data.append({
'Scenario': name,
'Composition': 'Blend',
'Value': comparison_df.loc[name].get(blend_prop_name, 0)
})
composite_df = pd.DataFrame(chart_data)
fig_composite = px.line(
composite_df, x='Composition', y='Value', color='Scenario',
markers=True, title=f"<b>Comparative Analysis for Property {prop_idx}</b>",
labels={'Composition': 'Composition (C1-C5 & Blend)', 'Value': f'Property {prop_idx} Value'}
)
st.plotly_chart(fig_composite, use_container_width=True)
# --- ADD: Floating Help Button for Blend Comparison ---
st.markdown("""
<style>
#help-toggle-comparison { display: none; }
#help-toggle-comparison:checked ~ .help-panel-comparison {
opacity: 1; visibility: visible; transform: translateY(0);
}
.help-panel-comparison {
position:fixed; right:25px; bottom:100px; z-index:9998;
width:520px; max-height:70vh; overflow-y:auto;
background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
border:1px solid #CFB53B; border-radius:12px; padding:20px;
box-shadow:0 14px 34px rgba(0,0,0,0.22);
color:#4a2f1f; transform: translateY(12px); opacity:0;
visibility:hidden; transition: all .22s ease-in-out;
}
</style>
<input id="help-toggle-comparison" type="checkbox" />
<label for="help-toggle-comparison" class="help-button">💬 Help</label>
<div class="help-panel help-panel-comparison">
<div class="head">
<div class="title">Using the Blend Comparison Tool</div>
<label for="help-toggle-comparison" class="help-close">Close</label>
</div>
<div class="help-body">
<p>This tab allows you to perform a side-by-side analysis of up to three saved blends.</p>
<p><b>1. Select Scenarios:</b> Use the three dropdown menus at the top to select the saved blends you wish to compare.</p>
<p><b>2. Review Overviews:</b> Key information for each selected blend, including its composition and final properties, will be displayed in summary cards.</p>
<p><b>3. Analyze Charts:</b> The charts provide a deep dive into how the blends compare on cost, property profiles, quality, and composition.</p>
<p><b>4. Export:</b> Click the 'Export to PDF' button to generate a downloadable report containing all the charts and data for your selected comparison.</p>
</div>
</div>
""", unsafe_allow_html=True)
# ----------------------------------------------------------------------------------------------------------------------------------------------
# Fuel Registry Tab
# ---------------------------------------------------------------------------------------------------------------------------------------------
def load_data(table_name: str, db_path="eagleblend.db") -> pd.DataFrame:
"""Loads data from a specified table in the database."""
try:
conn = sqlite3.connect(db_path)
# Assuming each table has a unique ID column as the first column
query = f"SELECT * FROM {table_name}"
df = pd.read_sql_query(query, conn)
return df
except Exception as e:
st.error(f"Failed to load data from table '{table_name}': {e}")
return pd.DataFrame()
def delete_records(table_name: str, ids_to_delete: list, id_column: str, db_path="eagleblend.db"):
"""Deletes records from a table based on a list of IDs."""
if not ids_to_delete:
return
conn = sqlite3.connect(db_path)
cur = conn.cursor()
try:
placeholders = ','.join('?' for _ in ids_to_delete)
query = f"DELETE FROM {table_name} WHERE {id_column} IN ({placeholders})"
cur.execute(query, ids_to_delete)
conn.commit()
finally:
conn.close()
@st.cache_data
def get_template(file_path):
"""Loads a template file into bytes for downloading."""
with open(file_path, 'rb') as f:
return f.read()
with tabs[4]:
st.subheader("📚 Fuel Registry")
st.write("Manage fuel components and blends. Add new entries manually, upload in batches, or download templates.")
# --- State Initialization ---
if 'components' not in st.session_state:
st.session_state.components = load_data('components')
if 'blends' not in st.session_state:
st.session_state.blends = load_data('blends')
# --- Section 1: Data Management (Uploads & Manual Entry) ---
col1, col2 = st.columns(2)
with col1:
with st.container(border=True):
st.markdown("#### ➕ Add Components")
with st.expander("Add a Single Component Manually"):
with st.form("new_component_form", clear_on_submit=True):
component_name = st.text_input("Component Name", placeholder="e.g., Reformate")
# --- FIX: Add Component Type dropdown ---
component_type = st.selectbox("Component Type", options=["-- Select a Type --", 1, 2, 3, 4, 5])
c_cols = st.columns(2)
# Gather all property inputs
property1 = c_cols[0].number_input("Property 1", value=0.0, step=0.1, format="%.4f")
property2 = c_cols[1].number_input("Property 2", value=0.0, step=0.1, format="%.4f")
property3 = c_cols[0].number_input("Property 3", value=0.0, step=0.1, format="%.4f")
property4 = c_cols[1].number_input("Property 4", value=0.0, step=0.1, format="%.4f")
property5 = c_cols[0].number_input("Property 5", value=0.0, step=0.1, format="%.4f")
property6 = c_cols[1].number_input("Property 6", value=0.0, step=0.1, format="%.4f")
property7 = c_cols[0].number_input("Property 7", value=0.0, step=0.1, format="%.4f")
property8 = c_cols[1].number_input("Property 8", value=0.0, step=0.1, format="%.4f")
property9 = c_cols[0].number_input("Property 9", value=0.0, step=0.1, format="%.4f")
property10 = c_cols[1].number_input("Property 10", value=0.0, step=0.1, format="%.4f")
unit_cost = c_cols[0].number_input("Unit Cost", value=0.0, step=0.1, format="%.2f")
if st.form_submit_button("💾 Save Component", use_container_width=True):
# --- FIX: Add validation for component type ---
if not component_name.strip():
st.warning("Component Name cannot be empty.")
elif component_type == "-- Select a Type --":
st.warning("Please select a Component Type.")
else:
# --- FIX: Include component_type in the data to be saved ---
new_component_data = {
"component_name": component_name,
"component_type": component_type,
"property1": property1, "property2": property2,
"property3": property3, "property4": property4,
"property5": property5, "property6": property6,
"property7": property7, "property8": property8,
"property9": property9, "property10": property10,
"unit_cost": unit_cost
}
new_component_df = pd.DataFrame([new_component_data])
rows_added = add_components(new_component_df)
if rows_added > 0:
st.success(f"Component '{component_name}' added successfully!")
if 'components' in st.session_state:
del st.session_state.components
st.rerun()
# Batch upload for components
st.markdown("---")
st.markdown("**Batch Upload Components**")
uploaded_components = st.file_uploader(
"Upload Components CSV", type=['csv'], key="components_uploader",
help="Upload a CSV file with component properties."
)
if uploaded_components:
try:
df = pd.read_csv(uploaded_components)
rows_added = add_components(df)
st.success(f"Successfully added {rows_added} new components to the registry!")
del st.session_state.components # Force reload
st.rerun()
except Exception as e:
st.error(f"Error processing file: {e}")
st.download_button(
label="📥 Download Component Template",
data=get_template('assets/components_template.csv'),
file_name='components_template.csv',
mime='text/csv',
use_container_width=True
)
with col2:
with st.container(border=True):
st.markdown("#### 🧬 Add Blends")
st.info("Upload blend compositions via CSV. Manual entry is not supported for blends.", icon="ℹ️")
# Batch upload for blends
uploaded_blends = st.file_uploader(
"Upload Blends CSV", type=['csv'], key="blends_uploader",
help="Upload a CSV file defining blend recipes."
)
if uploaded_blends:
try:
df = pd.read_csv(uploaded_blends)
rows_added = add_blends(df) # Assumes you have an add_blends function
st.success(f"Successfully added {rows_added} new blends to the registry!")
del st.session_state.blends # Force reload
st.rerun()
except Exception as e:
st.error(f"Error processing file: {e}")
st.download_button(
label="📥 Download Blend Template",
data=get_template('assets/blends_template.csv'),
file_name='blends_template.csv',
mime='text/csv',
use_container_width=True
)
st.divider()
# --- Section 2: Data Display & Deletion ---
st.markdown("#### 🔍 View & Manage Registry Data")
view_col1, view_col2 = st.columns([1, 2])
with view_col1:
table_to_show = st.selectbox(
"Select Table to View",
("Components", "Blends"),
label_visibility="collapsed"
)
with view_col2:
search_query = st.text_input(
"Search Table",
placeholder=f"Type to search in {table_to_show}...",
label_visibility="collapsed"
)
# Determine which DataFrame to use
if table_to_show == "Components":
df_display = st.session_state.components.copy()
id_column = "id" # Change if your ID column is named differently
else:
df_display = st.session_state.blends.copy()
id_column = "id" # Change if your ID column is named differently
# Apply search filter if query is provided
if search_query:
# A simple search across all columns
df_display = df_display[df_display.apply(
lambda row: row.astype(str).str.contains(search_query, case=False).any(),
axis=1
)]
if df_display.empty:
st.warning(f"No {table_to_show.lower()} found matching your criteria.")
else:
# Add a "Select" column for deletion
df_display.insert(0, "Select", False)
# Use data_editor to make the checkboxes interactive
edited_df = st.data_editor(
df_display,
hide_index=True,
use_container_width=True,
disabled=df_display.columns.drop("Select"), # Make all columns except "Select" read-only
key=f"editor_{table_to_show}"
)
selected_rows = edited_df[edited_df["Select"]]
if not selected_rows.empty:
# --- FIX: Reverted to the full-width button as requested ---
if st.button(f"❌ Delete Selected {table_to_show} ({len(selected_rows)})", use_container_width=False, type="primary"):
ids_to_del = selected_rows['id'].tolist()
delete_records(table_to_show.lower(), ids_to_del, 'id')
st.success(f"Deleted {len(ids_to_del)} records from {table_to_show}.")
# Clear the relevant cache to reflect the deletion
if table_to_show == "Components":
if 'components' in st.session_state:
del st.session_state.components
else:
if 'blends' in st.session_state:
del st.session_state.blends
st.rerun()
# st.rerun()
# --- ADD: Floating Help Button for Fuel Registry ---
st.markdown("""
<style>
#help-toggle-registry { display: none; }
#help-toggle-registry:checked ~ .help-panel-registry {
opacity: 1; visibility: visible; transform: translateY(0);
}
.help-panel-registry {
position:fixed; right:25px; bottom:100px; z-index:9998;
width:520px; max-height:70vh; overflow-y:auto;
background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
border:1px solid #CFB53B; border-radius:12px; padding:20px;
box-shadow:0 14px 34px rgba(0,0,0,0.22);
color:#4a2f1f; transform: translateY(12px); opacity:0;
visibility:hidden; transition: all .22s ease-in-out;
}
</style>
<input id="help-toggle-registry" type="checkbox" />
<label for="help-toggle-registry" class="help-button">💬 Help</label>
<div class="help-panel help-panel-registry">
<div class="head">
<div class="title">Using the Fuel Registry</div>
<label for="help-toggle-registry" class="help-close">Close</label>
</div>
<div class="help-body">
<p>This tab is your central database for managing all blend components and saved blends.</p>
<p><b>1. Add Components/Blends:</b> You can add a single component manually using the form or upload a CSV file for batch additions of components or blends. Download the templates to ensure your file format is correct.</p>
<p><b>2. View & Manage Data:</b> Use the dropdown to switch between viewing 'Components' and 'Blends'. The table shows all saved records.</p>
<p><b>3. Search & Delete:</b> Use the search bar to filter the table. To delete records, check the 'Select' box next to the desired rows and click the 'Delete Selected' button that appears.</p>
</div>
</div>
""", unsafe_allow_html=True)
# ----------------------------------------------------------------------------------------------------------------------------------------------
# Model Insights Tab
# ----------------------------------------------------------------------------------------------------------------------------------------------
with tabs[5]:
model_metrics = last_model[
[f"BlendProperty{i}_Score" for i in range(1, 11)]
]
# --- UI Rendering Starts Here ---
# Inject CSS for consistent styling with the rest of the app
st.markdown("""
<style>
/* Metric card styles */
.metric-card {
background: linear-gradient(180deg, #FFF8E1 0%, #FFF6EA 100%);
border: 1px solid #E3C77A;
border-radius: 8px;
padding: 15px;
text-align: center;
color: #654321;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
.metric-label {
font-size: 14px;
font-weight: 700;
color: #8B4513;
margin-bottom: 5px;
}
.metric-value {
font-size: 1.8rem;
font-weight: 900;
color: #4a2f1f;
}
/* Floating help button and panel styles */
#help-toggle{display:none;}
.help-button{
position:fixed; right:25px; bottom:25px; z-index:9999;
background:#8B4513; color:#FFD700; padding:16px 22px; font-size:17px;
border-radius:18px; font-weight:900; box-shadow:0 8px 22px rgba(0,0,0,0.2); cursor:pointer;
border:0;
}
.help-panel{
position:fixed; right:25px; bottom:100px; z-index:9998;
width:520px; max-height:70vh; overflow-y:auto;
background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
border:1px solid #CFB53B; border-radius:12px; padding:20px; box-shadow:0 14px 34px rgba(0,0,0,0.22);
color:#4a2f1f; transform: translateY(12px); opacity:0; visibility:hidden; transition: all .22s ease-in-out;
}
#help-toggle:checked + label.help-button + .help-panel{
opacity:1; visibility:visible; transform: translateY(0);
}
.help-panel .head{display:flex; justify-content:space-between; align-items:center; margin-bottom:12px}
.help-panel .title{font-weight:900; color:#654321; font-size:16px}
.help-close{background:#8B4513; color:#FFD700; padding:6px 10px; border-radius:8px; cursor:pointer; font-weight:800}
.help-body{font-size:14.5px; color:#4a2f1f; line-height:1.5}
.help-body b {color: #654321;}
</style>
""", unsafe_allow_html=True)
# # --- Floating "How to Use" Button and Panel ---
# st.markdown("""
# <input id="help-toggle" type="checkbox" />
# <label for="help-toggle" class="help-button">💬 Help</label>
# <div class="help-panel" aria-hidden="true">
# <div class="head">
# <div class="title">Interpreting Model Insights</div>
# <label for="help-toggle" class="help-close">Close</label>
# </div>
# <div class="help-body">
# <p><b>KPI Cards:</b> These four cards give you a quick summary of the model's overall health.</p>
# <ul>
# <li><b>Overall R² Score:</b> Think of this as the model's accuracy grade. A score of 92.4% means the model's predictions are highly accurate.</li>
# <li><b>MSE (Mean Squared Error):</b> This measures the average size of the model's mistakes. A smaller number is better.</li>
# <li><b>MAPE (Mean Absolute % Error):</b> This tells you the average error in percentage terms. A value of 0.112 means predictions are off by about 11.2% on average.</li>
# </ul>
# <p><b>R² Score by Blend Property Chart:</b> This chart shows how well the model predicts each specific property.</p>
# <p>A <b>longer bar</b> means the model is very good at predicting that property. A <b>shorter bar</b> indicates a property that is harder for the model to predict accurately. This helps you trust predictions for some properties more than others.</p>
# </div>
# </div>
# """, unsafe_allow_html=True)
# --- FIX: Complete working version of the help button ---
# --- FIX: Complete working version of the help button ---
st.markdown("""
<style>
/* Styles for the help panel and button */
#help-toggle-insights { display: none; }
#help-toggle-insights:checked ~ .help-panel-insights {
opacity: 1; visibility: visible; transform: translateY(0);
}
.help-panel-insights {
position:fixed; right:25px; bottom:100px; z-index:9998;
width:520px; max-height:70vh; overflow-y:auto;
background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
border:1px solid #CFB53B; border-radius:12px; padding:20px;
box-shadow:0 14px 34px rgba(0,0,0,0.22);
color:#4a2f1f; transform: translateY(12px); opacity:0;
visibility:hidden; transition: all .22s ease-in-out;
}
</style>
<input id="help-toggle-insights" type="checkbox" />
<label for="help-toggle-insights" class="help-button">💬 Help</label>
<div class="help-panel help-panel-insights">
<div class="head">
<div class="title">Interpreting Model Insights</div>
<label for="help-toggle-insights" class="help-close">Close</label>
</div>
<div class="help-body">
<p><b>KPI Cards:</b> These cards give a quick summary of the model's health. <b>R² Score</b> is its accuracy grade, while <b>MSE</b> and <b>MAPE</b> measure the average size of its errors.</p>
<p><b>R² Score by Blend Property Chart:</b> This chart shows how well the model predicts each specific property. A longer bar means the model is very good at predicting that property.</p>
</div>
</div>
""", unsafe_allow_html=True)
# --- Main Title ---
st.markdown('<h2 style="color:#4a2f1f; font-size:1.75rem;">🧠 Model Insights</h2>', unsafe_allow_html=True)
# --- Fetch Model Data ---
latest_model = get_model()
model_name = latest_model.get("model_name", "N/A")
r2_score = f'{latest_model.get("R2_Score", 0) * 100:.1f}%'
mse = f'{latest_model.get("MSE", 0):.3f}'
mape = f'{latest_model.get("MAPE", 0):.3f}'
# --- KPI Cards Section ---
k1, k2, k3, k4 = st.columns(4)
with k1:
st.markdown(f"""
<div class="metric-card">
<div class="metric-label">Model Name</div>
<div class="metric-value" style="font-size: 1.2rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{model_name}</div>
</div>
""", unsafe_allow_html=True)
with k2:
st.markdown(f"""
<div class="metric-card">
<div class="metric-label">Overall R² Score</div>
<div class="metric-value">{r2_score}</div>
</div>
""", unsafe_allow_html=True)
with k3:
st.markdown(f"""
<div class="metric-card">
<div class="metric-label">Mean Squared Error</div>
<div class="metric-value">{mse}</div>
</div>
""", unsafe_allow_html=True)
with k4:
st.markdown(f"""
<div class="metric-card">
<div class="metric-label">Mean Absolute % Error</div>
<div class="metric-value">{mape}</div>
</div>
""", unsafe_allow_html=True)
st.markdown('<div style="height:20px;"></div>', unsafe_allow_html=True) # Spacer
# --- R2 Score by Property Chart ---
st.markdown('<h3 style="color:#4a2f1f; font-size:1.5rem;">R² Score by Blend Property</h3>', unsafe_allow_html=True)
# Create the horizontal bar chart
fig_r2 = go.Figure()
fig_r2.add_trace(go.Bar(
y=model_metrics.index,
x=model_metrics.values,
orientation='h',
marker=dict(
color=model_metrics.values,
colorscale='YlOrBr',
colorbar=dict(title="R² Score", tickfont=dict(color="#4a2f1f")),
),
text=[f'{val:.2f}' for val in model_metrics.values],
textposition='inside',
insidetextanchor='middle',
textfont=dict(color='#4a2f1f', size=12, family='Arial, sans-serif', weight='bold')
))
# This corrected block resolves the ValueError
fig_r2.update_layout(
xaxis_title="R² Score (Higher is Better)",
yaxis_title="Blend Property",
plot_bgcolor='rgba(0,0,0,0)',
paper_bgcolor='rgba(0,0,0,0)',
margin=dict(l=10, r=10, t=20, b=50),
font=dict(
family="Segoe UI, Arial, sans-serif",
size=12,
color="#4a2f1f"
),
yaxis=dict(
tickfont=dict(size=12, weight='bold'),
automargin=True,
# FIX: The title font styling is now correctly nested here
title_font=dict(size=14)
),
xaxis=dict(
gridcolor="rgba(139, 69, 19, 0.2)",
zerolinecolor="rgba(139, 69, 19, 0.3)",
# FIX: The title font styling is now correctly nested here
title_font=dict(size=14)
)
)
st.plotly_chart(fig_r2, 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)