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