diff --git "a/test_app.py" "b/test_app.py" --- "a/test_app.py" +++ "b/test_app.py" @@ -1,2350 +1,2450 @@ -import streamlit as st -# import torch - -# # Give torch.classes a benign __path__ so Streamlit won't trigger __getattr__. -# try: -# setattr(torch.classes, "__path__", []) -# except Exception: -# # Fallback wrapper if direct setattr isn't allowed in your build -# class _TorchClassesWrapper: -# def __init__(self, obj): -# self._obj = obj -# self.__path__ = [] -# def __getattr__(self, name): -# return getattr(self._obj, name) -# torch.classes = _TorchClassesWrapper(torch.classes) - - - -if "modules_loaded" not in st.session_state: - # Do your big imports or setup here - # import torch - from predictor import EagleBlendPredictor - 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 - - st.session_state["modules_loaded"] = True - - -##---- fucntions ------ -# Load fuel data from CSV (create this file if it doesn't exist) -FUEL_CSV_PATH = "fuel_properties.csv" - -def load_fuel_data(): - """Load fuel data from CSV or create default if not exists""" - try: - df = pd.read_csv(FUEL_CSV_PATH, index_col=0) - return df.to_dict('index') - except FileNotFoundError: - # Create default fuel properties if file doesn't exist - default_fuels = { - "Gasoline": {f"Property{i+1}": round(0.7 + (i*0.02), 1) for i in range(10)}, - "Diesel": {f"Property{i+1}": round(0.8 + (i*0.02), 1) for i in range(10)}, - "Ethanol": {f"Property{i+1}": round(0.75 + (i*0.02), 1) for i in range(10)}, - "Biodiesel": {f"Property{i+1}": round(0.85 + (i*0.02), 1) for i in range(10)}, - "Jet Fuel": {f"Property{i+1}": round(0.78 + (i*0.02), 1) for i in range(10)} - } - pd.DataFrame(default_fuels).T.to_csv(FUEL_CSV_PATH) - return default_fuels - -# Initialize or load fuel data -if 'FUEL_PROPERTIES' not in st.session_state: - st.session_state.FUEL_PROPERTIES = load_fuel_data() - -def save_fuel_data(): - """Save current fuel data to CSV""" - pd.DataFrame(st.session_state.FUEL_PROPERTIES).T.to_csv(FUEL_CSV_PATH) - -# FUEL_PROPERTIES = st.session_state.FUEL_PROPERTIES - -# ---------------------- Page Config ---------------------- -st.set_page_config( - layout="wide", - page_title="Eagle Blend Optimizer", - page_icon="๐Ÿฆ…", - initial_sidebar_state="expanded" -) - -# ---------------------- Custom Styling ---------------------- ##e0e0e0; - -st.markdown(""" - -""", unsafe_allow_html=True) - -# ---------------------- App Header ---------------------- -st.markdown(""" -
-

๐Ÿฆ… Eagle Blend Optimizer

-

- AI-Powered Fuel Blend Property Prediction & Optimization -

-
-""", 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, - "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('

Performance Summary

', unsafe_allow_html=True) - k1, k2, k3, k4 = st.columns(4) - with k1: - st.markdown(f""" -
-
Model Accuracy
-
{r2_display}
-
Rยฒ (latest)
-
- """, unsafe_allow_html=True) - with k2: - st.markdown(f""" -
-
Predictions Made
-
{preds}
-
This Week
-
- """, unsafe_allow_html=True) - with k3: - st.markdown(f""" -
-
Optimizations
-
{opts}
-
This Week
-
- """, unsafe_allow_html=True) - with k4: - st.markdown(f""" -
-
Highest Cost Savings
-
{max_saving_display}
-
Per unit fuel
-
- """, unsafe_allow_html=True) - - st.markdown('
', unsafe_allow_html=True) - - # ---------- Floating "How to Use" (bigger button + inline content) + compact CSS ---------- - st.markdown(""" - - - - - - - """, unsafe_allow_html=True) - - # ---------- 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('

Blend Entries Per Day

', 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('
', unsafe_allow_html=True) - st.markdown('
๐Ÿ—’๏ธ Recent Blends
', 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'C{i}: {float(val)*100:.0f}%' - - 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""" -
-
-
{name}
-
{ts}
-
-
{comp_html}
-
{props_html}
-
- """, unsafe_allow_html=True) - - st.markdown('
', 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_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() - - # 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]}") - 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}") ] - - 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('
', 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 --- - if st.button("๐Ÿงช Predict Blended Properties", use_container_width=False, type="primary"): - handle_prediction() - - # --- Results Section --- - if st.session_state.get('prediction_made', False): - st.markdown('
', 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""" -
-
Blend Property {i+1}
-
{results_array[i]:.4f}
-
- """, 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""" -
-
Predicted Blend Cost
-
{cost_display}
-
{delta_text}
-
- """, 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('
', unsafe_allow_html=True) - - # --- Floating "How to Use" button --- - st.markdown(""" - - - -
-
-
Using the Blend Designer
- -
-

1. Configure Components: For each of the 5 components, you can either...

- -

2. Predict: Once fractions sum to 1.0, click Predict to see the results.

-

3. Save: After predicting, enter a unique Blend Name and click Save to store it in the database.

-
- """, unsafe_allow_html=True) - -# ---------------------------------------------------------------------------------------------------------------------------------------------- -# Optimization Engine Tab -# ---------------------------------------------------------------------------------------------------------------------------------------------- - - - -# --- Add this new function to your functions section --- -def dummy_optimization_function(targets, fixed_targets, components_data): - """ - Placeholder for your actual optimization algorithm. - This function simulates a multi-objective optimization. - - Returns: - A list of dictionaries, where each dictionary represents a solution. - """ - print("--- Running Dummy Optimization ---") - print("Targets:", targets) - print("Fixed Targets:", fixed_targets) - print("---------------------------------") - - # Simulate a process that takes a few seconds - time.sleep(3) - - # Generate 3 dummy solutions - solutions = [] - for i in range(3): - # Create slightly different results for each solution - base_frac = 0.2 + (i * 0.05) - fractions = np.random.rand(5) - fractions = fractions / fractions.sum() # Normalize to sum to 1 - - blend_properties = [val + np.random.uniform(-0.5, 0.5) for val in targets.values()] - - # Ensure fixed targets are met in the dummy result - for prop, val in fixed_targets.items(): - prop_index = int(prop.replace('Property', '')) - 1 - blend_properties[prop_index] = val - - solution = { - "component_fractions": fractions, - "blend_properties": np.array(blend_properties), - "optimized_cost": 150.0 - (i * 10), - "error": 0.05 + (i * 0.02) # Dummy error for the Pareto plot - } - solutions.append(solution) - - return solutions - - -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() - 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() - 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") - - # Run button and spinner logic - run_button_col, spinner_col = st.columns([3, 1]) - with run_button_col: - if st.button("๐Ÿš€ Run Optimization", use_container_width=True, type="primary", disabled=st.session_state.optimization_running): - st.session_state.optimization_running = True - start_time = time.time() - - # Gather data for the optimization function - targets = {f"Property{i}": st.session_state[f"opt_target_{i}"] 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[f"opt_fix_{i}"]} - components_data = [] # You would gather component data similarly if your function needs it - - # Call the (dummy) optimization function - st.session_state.optimization_results = dummy_optimization_function(targets, fixed_targets, components_data) - st.session_state.optimization_time = time.time() - start_time - st.session_state.optimization_running = False - st.rerun() # Rerun to display results - - with spinner_col: - if st.session_state.optimization_running: - st.markdown('
', unsafe_allow_html=True) - - if st.session_state.optimization_time > 0: - st.success(f"Optimization complete in {st.session_state.optimization_time:.2f} seconds.") - - # --- Results Section --- - if st.session_state.optimization_results: - st.markdown('
', unsafe_allow_html=True) - st.subheader("๐Ÿ† Optimization Results") - - results = st.session_state.optimization_results - - # Dropdown to select which result to view - result_options = {i: f"Solution {i+1}" for i in range(len(results))} - selected_idx = st.selectbox("View Solution", options=list(result_options.keys()), format_func=lambda x: result_options[x]) - - selected_solution = results[selected_idx] - - # Display best fractions and properties - res_cols = st.columns([3, 2]) - with res_cols[0]: - st.markdown("##### Optimal Component Fractions") - frac_cols = st.columns(5) - for i, frac in enumerate(selected_solution["component_fractions"]): - with frac_cols[i]: - comp_name = st.session_state.get(f"opt_c{i}_name", f"Component {i+1}") - st.markdown(f""" -
-
{comp_name}
-
{frac*100:.2f}%
-
- """, unsafe_allow_html=True) - - # --- FIX: New, readable KPI cards for blend properties --- - with res_cols[1]: - st.markdown("##### Resulting Blend Properties") - prop_kpi_cols = st.columns(5) - for i, prop_val in enumerate(selected_solution["blend_properties"]): - col = prop_kpi_cols[i % 5] - with col: - st.markdown(f""" -
-
Property {i+1}
-
{prop_val:.4f}
-
- """, unsafe_allow_html=True) - - # 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="Pareto Front: Error vs. Cost" - ) - 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") - if st.button("๐Ÿ’พ Save to Database", use_container_width=True): - st.info("Save functionality can be implemented here.") # Placeholder for save logic - - # Placeholder for download button logic - st.download_button("๐Ÿ“ฅ Download All Solutions (CSV)", data="dummy_csv_data", file_name="optimization_results.csv", use_container_width=True) - - # --- Floating Help Button --- - # (Using a different key to avoid conflict with other tabs) - # --- FIX: Complete working version of the help button --- - st.markdown(""" - - - -
-
How to Use the Optimizer
- -
-
-

1. Define Goals: 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.

-

2. Select Components: Choose up to 5 base components. You can load them from the registry to auto-fill their data or enter them manually.

-

3. Configure & Run: Decide if cost should be a factor in the optimization, then click 'Run Optimization'. A spinner will appear while the process runs.

-

4. Analyze Results: 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.

-

5. Save & Download: Give your chosen solution a name and save it to the blends database for future use in the Comparison tab.

-
-
- """, unsafe_allow_html=True) - -# ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -# Blend Comparison Tab -# ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -# --- Add these two new functions to your main script's function section --- - -@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 - -@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: - comparison_df = all_blends_df[all_blends_df['blend_name'].isin(selected_blends)].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] - created_at = pd.to_datetime(blend_data.get('created_at')).strftime('%Y-%m-%d') if blend_data.get('created_at') 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"C{j}: {frac:.1f}%   " - - # 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"P{j}: {prop:.3f}" - - st.markdown(f""" -
-
{blend_name}
-
Created: {created_at}
-
{fractions_html}
-
{properties_html}
-
- """, unsafe_allow_html=True) - - # --- Charting Section --- - st.markdown('
', unsafe_allow_html=True) - st.subheader("๐Ÿ“ˆ Comparative Analysis") - - plot_cols = st.columns(2) - with plot_cols[0]: - # --- Plot 1: Lollipop Plot (Cost) --- - costs = [] - for name in selected_blends: - row = comparison_df.loc[name] - cost = row.get('Optimized_Cost', 0) - if not cost or cost == 0: - cost = row.get('PreOpt_Cost', 0) - costs.append(cost) - - # --- This is the corrected block --- - fig_cost = go.Figure() - # Use a thin Bar trace for the lollipop stems - fig_cost.add_trace(go.Bar( - x=selected_blends, y=costs, - marker_color='#CFB53B', - width=0.05, # Make the bars very thin to act as stems - name='Cost Stem' - )) - # Add the 'lollipops' (the dots) on top - fig_cost.add_trace(go.Scatter( - x=selected_blends, y=costs, mode='markers', - marker=dict(color='#8B4513', size=12), - name='Cost Value' - )) - fig_cost.update_layout( - title="Blend Cost Comparison", yaxis_title="Cost ($)", - showlegend=False, plot_bgcolor='rgba(0,0,0,0)' - ) - st.plotly_chart(fig_cost, use_container_width=True) - - # --- 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="Blend Property Profile", - 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) --- - quality_scores = [comparison_df.loc[name].get('Quality_Score', 0) for name in selected_blends] - - fig_scatter = px.scatter( - x=costs, y=quality_scores, text=selected_blends, - labels={'x': 'Cost ($)', 'y': 'Quality Score'}, - title="Cost vs. Quality Frontier" - ) - fig_scatter.update_traces( - textposition='top center', - marker=dict(size=15, color='#8B4513', symbol='diamond') - ) - st.plotly_chart(fig_scatter, use_container_width=True) - - 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="Component Composition by Scenario", - 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('
', 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"Comparative Analysis for Property {prop_idx}", - 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(""" - - - -
-
-
Using the Blend Comparison Tool
- -
-
-

This tab allows you to perform a side-by-side analysis of up to three saved blends.

-

1. Select Scenarios: Use the three dropdown menus at the top to select the saved blends you wish to compare.

-

2. Review Overviews: Key information for each selected blend, including its composition and final properties, will be displayed in summary cards.

-

3. Analyze Charts: The charts provide a deep dive into how the blends compare on cost, property profiles, quality, and composition.

-

4. Export: Click the 'Export to PDF' button to generate a downloadable report containing all the charts and data for your selected comparison.

-
-
- """, 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") - - # Manual entry for a single component - 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") - # Add inputs for other key properties of a component - # This example assumes a few common properties. Adjust as needed. - c_cols = st.columns(2) - component_fraction = c_cols[1].number_input("Component Fraction", value=0.0, step=0.1, format="%.2f") - property1 = c_cols[0].number_input("Property1", value=0.0, step=0.1, format="%.2f") - property2 = c_cols[1].number_input("Property2", value=0.0, step=0.1, format="%.2f") - property3 = c_cols[0].number_input("Property3", value=0.0, step=0.1, format="%.2f") - property4 = c_cols[1].number_input("Property4", value=0.0, step=0.1, format="%.2f") - property5 = c_cols[0].number_input("Property5", value=0.0, step=0.1, format="%.2f") - property6 = c_cols[1].number_input("Property6", value=0.0, step=0.1, format="%.2f") - property7 = c_cols[0].number_input("Property 7", value=0.0, step=0.1, format="%.2f") - property8 = c_cols[1].number_input("Property 8", value=0.0, step=0.1, format="%.2f") - property9 = c_cols[0].number_input("Property 9", value=0.0, step=0.1, format="%.2f") - property10 = c_cols[1].number_input("Property 10", value=0.0, step=0.1, format="%.2f") - unit_cost = c_cols[0].number_input("unit_cost", value=0.0, step=0.1, format="%.2f") - # property4 = c_cols[1].number_input("Unit Cost", value=0.0, step=0.1, format="%.2f") - - if st.form_submit_button("๐Ÿ’พ Save Component", use_container_width=True): - if not component_name.strip(): - st.warning("Component Name cannot be empty.") - else: - new_component_df = pd.DataFrame([{ - "component_name": component_name, - "RON": ron, "MON": mon, "RVP": rvp, "Cost": cost - # Add other properties here - }]) - rows_added = add_components(new_component_df) - if rows_added > 0: - st.success(f"Component '{component_name}' added successfully!") - # Clear cache and rerun - 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 = "component_id" # Change if your ID column is named differently - else: - df_display = st.session_state.blends.copy() - id_column = "blend_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: - if st.button(f"โŒ Delete Selected {table_to_show} ({len(selected_rows)})", use_container_width=True, type="primary"): - ids_to_del = selected_rows[id_column].tolist() - delete_records(table_to_show.lower(), ids_to_del, id_column) - st.success(f"Deleted {len(ids_to_del)} records from {table_to_show}.") - # Force a data refresh - if table_to_show == "Components": - del st.session_state.components - else: - del st.session_state.blends - st.rerun() - - # --- ADD: Floating Help Button for Fuel Registry --- - st.markdown(""" - - - -
-
-
Using the Fuel Registry
- -
-
-

This tab is your central database for managing all blend components and saved blends.

-

1. Add Components/Blends: 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.

-

2. View & Manage Data: Use the dropdown to switch between viewing 'Components' and 'Blends'. The table shows all saved records.

-

3. Search & Delete: 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.

-
-
- """, 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(""" - - """, unsafe_allow_html=True) - - # # --- Floating "How to Use" Button and Panel --- - # st.markdown(""" - # - # - - # - # """, unsafe_allow_html=True) - - # --- FIX: Complete working version of the help button --- -# --- FIX: Complete working version of the help button --- - st.markdown(""" - - - -
-
-
Interpreting Model Insights
- -
-
-

KPI Cards: These cards give a quick summary of the model's health. Rยฒ Score is its accuracy grade, while MSE and MAPE measure the average size of its errors.

-

Rยฒ Score by Blend Property Chart: This chart shows how well the model predicts each specific property. A longer bar means the model is very good at predicting that property.

-
-
- """, unsafe_allow_html=True) - - # --- Main Title --- - st.markdown('

๐Ÿง  Model Insights

', 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""" -
-
Model Name
-
{model_name}
-
- """, unsafe_allow_html=True) - with k2: - st.markdown(f""" -
-
Overall Rยฒ Score
-
{r2_score}
-
- """, unsafe_allow_html=True) - with k3: - st.markdown(f""" -
-
Mean Squared Error
-
{mse}
-
- """, unsafe_allow_html=True) - with k4: - st.markdown(f""" -
-
Mean Absolute % Error
-
{mape}
-
- """, unsafe_allow_html=True) - - st.markdown('
', unsafe_allow_html=True) # Spacer - - # --- R2 Score by Property Chart --- - st.markdown('

Rยฒ Score by Blend Property

', 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(""" -# + +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 +from inference import EagleBlendPredictor + + +import torch + +# Give torch.classes a benign __path__ so Streamlit won't trigger __getattr__. +try: + setattr(torch.classes, "__path__", []) +except Exception: + # Fallback wrapper if direct setattr isn't allowed in your build + class _TorchClassesWrapper: + def __init__(self, obj): + self._obj = obj + self.__path__ = [] + def __getattr__(self, name): + return getattr(self._obj, name) + torch.classes = _TorchClassesWrapper(torch.classes) + +import streamlit as st + +##---- fucntions ------ +# Load fuel data from CSV (create this file if it doesn't exist) +FUEL_CSV_PATH = "fuel_properties.csv" + +def load_fuel_data(): + """Load fuel data from CSV or create default if not exists""" + try: + df = pd.read_csv(FUEL_CSV_PATH, index_col=0) + return df.to_dict('index') + except FileNotFoundError: + # Create default fuel properties if file doesn't exist + default_fuels = { + "Gasoline": {f"Property{i+1}": round(0.7 + (i*0.02), 1) for i in range(10)}, + "Diesel": {f"Property{i+1}": round(0.8 + (i*0.02), 1) for i in range(10)}, + "Ethanol": {f"Property{i+1}": round(0.75 + (i*0.02), 1) for i in range(10)}, + "Biodiesel": {f"Property{i+1}": round(0.85 + (i*0.02), 1) for i in range(10)}, + "Jet Fuel": {f"Property{i+1}": round(0.78 + (i*0.02), 1) for i in range(10)} + } + pd.DataFrame(default_fuels).T.to_csv(FUEL_CSV_PATH) + return default_fuels + +# Initialize or load fuel data +if 'FUEL_PROPERTIES' not in st.session_state: + st.session_state.FUEL_PROPERTIES = load_fuel_data() + +def save_fuel_data(): + """Save current fuel data to CSV""" + pd.DataFrame(st.session_state.FUEL_PROPERTIES).T.to_csv(FUEL_CSV_PATH) + +# FUEL_PROPERTIES = st.session_state.FUEL_PROPERTIES + +# ---------------------- Page Config ---------------------- +st.set_page_config( + layout="wide", + page_title="Eagle Blend Optimizer", + page_icon="๐Ÿฆ…", + initial_sidebar_state="expanded" +) + + +# ---------------------- 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(""" + +""", unsafe_allow_html=True) + +# ---------------------- App Header ---------------------- +# --- This is the new header with the subtitle --- +st.markdown(""" +
+

๐Ÿฆ… Eagle Blend Optimizer

+

+ AI-Powered Fuel Blend Property Prediction & Optimization +

+

+ by eagle-team for the Shell.ai 2025 Hackathon +

+
+""", 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, + "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]: + import math + import plotly.graph_objects as go + + # 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('

Performance Summary

', unsafe_allow_html=True) + k1, k2, k3, k4 = st.columns(4) + with k1: + st.markdown(f""" +
+
Model Accuracy
+
{r2_display}
+
Rยฒ (latest)
+
+ """, unsafe_allow_html=True) + with k2: + st.markdown(f""" +
+
Predictions Made
+
{preds}
+
This Week
+
+ """, unsafe_allow_html=True) + with k3: + st.markdown(f""" +
+
Optimizations
+
{opts}
+
This Week
+
+ """, unsafe_allow_html=True) + with k4: + st.markdown(f""" +
+
Highest Cost Savings
+
{max_saving_display}
+
Per unit fuel
+
+ """, unsafe_allow_html=True) + + st.markdown('
', unsafe_allow_html=True) + + # ---------- Floating "How to Use" (bigger button + inline content) + compact CSS ---------- + # st.markdown(""" + # + + # + # + + # + # """, unsafe_allow_html=True) + +# --- FIX: Removed extra blank lines inside the