Spaces:
Paused
Paused
Upload 4 files
Browse files- eagleblend.db +1 -1
- inference.py +209 -0
- predictor.py +35 -2
- test_app.py +492 -90
eagleblend.db
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
size 3096576
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:38cccc7257ad01d8d5ca0ac26a68090f4ac7c26fd2c3a76544fb27cfadb344e4
|
| 3 |
size 3096576
|
inference.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
import torch
|
| 4 |
+
import joblib
|
| 5 |
+
import argparse
|
| 6 |
+
import os
|
| 7 |
+
import glob
|
| 8 |
+
from sklearn.multioutput import MultiOutputRegressor
|
| 9 |
+
from tabpfn_extensions.post_hoc_ensembles.sklearn_interface import AutoTabPFNRegressor
|
| 10 |
+
|
| 11 |
+
class TabPFNEnsemblePredictor:
|
| 12 |
+
"""
|
| 13 |
+
A class to load an ensemble of TabPFN models and generate averaged predictions.
|
| 14 |
+
|
| 15 |
+
This class is designed to find and load all k-fold models from a specified
|
| 16 |
+
directory, handle the necessary feature engineering, and produce a single,
|
| 17 |
+
ensembled prediction from various input types (DataFrame, numpy array, or CSV file path).
|
| 18 |
+
|
| 19 |
+
Attributes:
|
| 20 |
+
model_paths (list): A list of file paths for the loaded models.
|
| 21 |
+
models (list): A list of the loaded model objects.
|
| 22 |
+
target_cols (list): The names of the target columns for the output DataFrame.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, model_dir: str, model_pattern: str = "Fold_*_best_model.tabpfn_fit"):
|
| 26 |
+
"""
|
| 27 |
+
Initializes the predictor by finding and loading the ensemble of models.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
model_dir (str): The directory containing the saved .tabpfn_fit model files.
|
| 31 |
+
model_pattern (str, optional): The glob pattern to find model files.
|
| 32 |
+
Defaults to "Fold_*_best_model.tabpfn_fit".
|
| 33 |
+
|
| 34 |
+
Raises:
|
| 35 |
+
FileNotFoundError: If no models matching the pattern are found in the directory.
|
| 36 |
+
"""
|
| 37 |
+
print("Initializing the TabPFN Ensemble Predictor...")
|
| 38 |
+
self.model_paths = sorted(glob.glob(os.path.join(model_dir, model_pattern)))
|
| 39 |
+
if not self.model_paths:
|
| 40 |
+
raise FileNotFoundError(
|
| 41 |
+
f"Error: No models found in '{model_dir}' matching the pattern '{model_pattern}'"
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
print(f"Found {len(self.model_paths)} models to form the ensemble.")
|
| 45 |
+
self.models = self._load_models()
|
| 46 |
+
self.target_cols = [f"BlendProperty{i}" for i in range(1, 11)]
|
| 47 |
+
|
| 48 |
+
def _load_models(self) -> list:
|
| 49 |
+
"""
|
| 50 |
+
Loads the TabPFN models from the specified paths and moves them to the CPU.
|
| 51 |
+
|
| 52 |
+
This is a private method called during initialization.
|
| 53 |
+
"""
|
| 54 |
+
loaded_models = []
|
| 55 |
+
for model_path in self.model_paths:
|
| 56 |
+
print(f"Loading model: {os.path.basename(model_path)}...")
|
| 57 |
+
try:
|
| 58 |
+
# Move model components to CPU for inference to avoid potential CUDA errors
|
| 59 |
+
# and ensure compatibility on machines without a GPU.
|
| 60 |
+
if not torch.cuda.is_available():
|
| 61 |
+
print("Cuda not available using cpu")
|
| 62 |
+
model = joblib.load(model_path)
|
| 63 |
+
for estimator in model.estimators_:
|
| 64 |
+
if hasattr(estimator, "predictor_") and hasattr(estimator.predictor_, "predictors"):
|
| 65 |
+
for p in estimator.predictor_.predictors:
|
| 66 |
+
p.to("cpu")
|
| 67 |
+
else:
|
| 68 |
+
print("Cuda is available")
|
| 69 |
+
model = joblib.load(model_path)
|
| 70 |
+
|
| 71 |
+
loaded_models.append(model)
|
| 72 |
+
print(f"Successfully loaded {os.path.basename(model_path)}")
|
| 73 |
+
except Exception as e:
|
| 74 |
+
print(f"Warning: Could not load model from {model_path}. Skipping. Error: {e}")
|
| 75 |
+
return loaded_models
|
| 76 |
+
|
| 77 |
+
@staticmethod
|
| 78 |
+
def _feature_engineering(df: pd.DataFrame) -> pd.DataFrame:
|
| 79 |
+
"""
|
| 80 |
+
Applies feature engineering to the input dataframe. This is a static method
|
| 81 |
+
as it does not depend on the state of the class instance.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
df (pd.DataFrame): The input dataframe.
|
| 85 |
+
|
| 86 |
+
Returns:
|
| 87 |
+
pd.DataFrame: The dataframe with new engineered features.
|
| 88 |
+
"""
|
| 89 |
+
components = ['Component1', 'Component2', 'Component3', 'Component4', 'Component5']
|
| 90 |
+
properties = [f'Property{i}' for i in range(1, 11)]
|
| 91 |
+
df_featured = df.copy()
|
| 92 |
+
|
| 93 |
+
for prop in properties:
|
| 94 |
+
df_featured[f'Weighted_{prop}'] = sum(
|
| 95 |
+
df_featured[f'{comp}_fraction'] * df_featured[f'{comp}_{prop}'] for comp in components
|
| 96 |
+
)
|
| 97 |
+
cols = [f'{comp}_{prop}' for comp in components]
|
| 98 |
+
df_featured[f'{prop}_variance'] = df_featured[cols].var(axis=1)
|
| 99 |
+
df_featured[f'{prop}_range'] = df_featured[cols].max(axis=1) - df_featured[cols].min(axis=1)
|
| 100 |
+
|
| 101 |
+
return df_featured
|
| 102 |
+
|
| 103 |
+
def predict(self, input_data: pd.DataFrame or np.ndarray or str) -> (np.ndarray, pd.DataFrame):
|
| 104 |
+
"""
|
| 105 |
+
Generates ensembled predictions for the given input data.
|
| 106 |
+
|
| 107 |
+
This method takes input data, preprocesses it if necessary, generates a
|
| 108 |
+
prediction from each model in the ensemble, and returns the averaged result.
|
| 109 |
+
|
| 110 |
+
Args:
|
| 111 |
+
input_data (pd.DataFrame or np.ndarray or str): The input data for prediction.
|
| 112 |
+
Can be a pandas DataFrame, a numpy array (must be pre-processed),
|
| 113 |
+
or a string path to a CSV file.
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
tuple: A tuple containing:
|
| 117 |
+
- np.ndarray: The averaged predictions as a numpy array.
|
| 118 |
+
- pd.DataFrame: The averaged predictions as a pandas DataFrame.
|
| 119 |
+
"""
|
| 120 |
+
if not self.models:
|
| 121 |
+
print("Error: No models were loaded. Cannot make predictions.")
|
| 122 |
+
return None, None
|
| 123 |
+
|
| 124 |
+
# --- Data Preparation ---
|
| 125 |
+
if isinstance(input_data, str) and os.path.isfile(input_data):
|
| 126 |
+
print(f"Loading and processing data from CSV: {input_data}")
|
| 127 |
+
test_df = pd.read_csv(input_data)
|
| 128 |
+
processed_df = self._feature_engineering(test_df)
|
| 129 |
+
elif isinstance(input_data, pd.DataFrame):
|
| 130 |
+
print("Processing input DataFrame...")
|
| 131 |
+
processed_df = self._feature_engineering(input_data)
|
| 132 |
+
elif isinstance(input_data, np.ndarray):
|
| 133 |
+
print("Using input numpy array directly (assuming it's pre-processed).")
|
| 134 |
+
sub = input_data
|
| 135 |
+
else:
|
| 136 |
+
raise TypeError("Input data must be a pandas DataFrame, a numpy array, or a path to a CSV file.")
|
| 137 |
+
|
| 138 |
+
if isinstance(input_data, (str, pd.DataFrame)):
|
| 139 |
+
if "ID" in processed_df.columns:
|
| 140 |
+
sub = processed_df.drop(columns=["ID"]).values
|
| 141 |
+
else:
|
| 142 |
+
sub = processed_df.values
|
| 143 |
+
|
| 144 |
+
# --- Prediction Loop ---
|
| 145 |
+
all_fold_predictions = []
|
| 146 |
+
print("\nGenerating predictions from the model ensemble...")
|
| 147 |
+
for i, model in enumerate(self.models):
|
| 148 |
+
try:
|
| 149 |
+
y_sub = model.predict(sub)
|
| 150 |
+
all_fold_predictions.append(y_sub)
|
| 151 |
+
print(f" - Prediction from model {i+1} completed.")
|
| 152 |
+
except Exception as e:
|
| 153 |
+
print(f" - Warning: Could not predict with model {i+1}. Skipping. Error: {e}")
|
| 154 |
+
|
| 155 |
+
if not all_fold_predictions:
|
| 156 |
+
print("\nError: No predictions were generated from any model.")
|
| 157 |
+
return None, None
|
| 158 |
+
|
| 159 |
+
# --- Averaging ---
|
| 160 |
+
print("\nAveraging predictions from all models...")
|
| 161 |
+
averaged_preds_array = np.mean(all_fold_predictions, axis=0)
|
| 162 |
+
averaged_preds_df = pd.DataFrame(averaged_preds_array, columns=self.target_cols)
|
| 163 |
+
print("Ensemble prediction complete.")
|
| 164 |
+
|
| 165 |
+
return averaged_preds_array, averaged_preds_df
|
| 166 |
+
|
| 167 |
+
# This block allows the script to be run directly from the command line
|
| 168 |
+
if __name__ == "__main__":
|
| 169 |
+
parser = argparse.ArgumentParser(
|
| 170 |
+
description="""
|
| 171 |
+
Command-line interface for the TabPFNEnsemblePredictor.
|
| 172 |
+
|
| 173 |
+
Example Usage:
|
| 174 |
+
python inference.py --model_dir ./saved_models/ --input_path ./test_data.csv --output_path ./final_preds.csv
|
| 175 |
+
""",
|
| 176 |
+
formatter_class=argparse.RawTextHelpFormatter
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
parser.add_argument("--model_dir", type=str, required=True,
|
| 180 |
+
help="Directory containing the saved .tabpfn_fit model files.")
|
| 181 |
+
parser.add_argument("--input_path", type=str, required=True,
|
| 182 |
+
help="Path to the input CSV file for prediction.")
|
| 183 |
+
parser.add_argument("--output_path", type=str, default="predictions_ensembled.csv",
|
| 184 |
+
help="Path to save the final ensembled predictions CSV file.")
|
| 185 |
+
|
| 186 |
+
args = parser.parse_args()
|
| 187 |
+
|
| 188 |
+
if not os.path.isdir(args.model_dir):
|
| 189 |
+
print(f"Error: Model directory not found at {args.model_dir}")
|
| 190 |
+
elif not os.path.exists(args.input_path):
|
| 191 |
+
print(f"Error: Input file not found at {args.input_path}")
|
| 192 |
+
else:
|
| 193 |
+
try:
|
| 194 |
+
# 1. Instantiate the predictor class
|
| 195 |
+
predictor = TabPFNEnsemblePredictor(model_dir=args.model_dir)
|
| 196 |
+
|
| 197 |
+
# 2. Call the predict method
|
| 198 |
+
preds_array, preds_df = predictor.predict(args.input_path)
|
| 199 |
+
|
| 200 |
+
# 3. Save the results
|
| 201 |
+
if preds_df is not None:
|
| 202 |
+
preds_df.to_csv(args.output_path, index=False)
|
| 203 |
+
print(f"\nEnsembled predictions successfully saved to {args.output_path}")
|
| 204 |
+
print("\n--- Sample of Final Averaged Predictions ---")
|
| 205 |
+
print(preds_df.head())
|
| 206 |
+
print("------------------------------------------")
|
| 207 |
+
|
| 208 |
+
except Exception as e:
|
| 209 |
+
print(f"\nAn error occurred during the process: {e}")
|
predictor.py
CHANGED
|
@@ -45,7 +45,7 @@ from scipy.special import comb
|
|
| 45 |
|
| 46 |
|
| 47 |
class EagleBlendPredictor:
|
| 48 |
-
def __init__(self, model_sources = '
|
| 49 |
"""
|
| 50 |
model_sources: Dict[str, Any]
|
| 51 |
A dictionary where keys are 'BlendProperty1', ..., 'BlendProperty10'
|
|
@@ -121,7 +121,7 @@ class EagleBlendPredictor:
|
|
| 121 |
self.model_10 = joblib.load(os.path.join(self.home, self.saved_files_map[10]["model"]))
|
| 122 |
self.poly_10 = joblib.load(os.path.join(self.home, self.saved_files_map[10]["transform"]))
|
| 123 |
|
| 124 |
-
self.model_3489 = TabPFNEnsemblePredictor(model_dir=
|
| 125 |
pass
|
| 126 |
|
| 127 |
|
|
@@ -260,6 +260,39 @@ class EagleBlendPredictor:
|
|
| 260 |
return predictions_df
|
| 261 |
|
| 262 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
|
| 265 |
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
class EagleBlendPredictor:
|
| 48 |
+
def __init__(self, model_sources = 'Models'):
|
| 49 |
"""
|
| 50 |
model_sources: Dict[str, Any]
|
| 51 |
A dictionary where keys are 'BlendProperty1', ..., 'BlendProperty10'
|
|
|
|
| 121 |
self.model_10 = joblib.load(os.path.join(self.home, self.saved_files_map[10]["model"]))
|
| 122 |
self.poly_10 = joblib.load(os.path.join(self.home, self.saved_files_map[10]["transform"]))
|
| 123 |
|
| 124 |
+
self.model_3489 = TabPFNEnsemblePredictor(model_dir=self.home)
|
| 125 |
pass
|
| 126 |
|
| 127 |
|
|
|
|
| 260 |
return predictions_df
|
| 261 |
|
| 262 |
|
| 263 |
+
def predict_fast(self, df: pd.DataFrame) -> pd.DataFrame:
|
| 264 |
+
"""
|
| 265 |
+
Generates predictions for the faster blend properties using the individual prediction methods.
|
| 266 |
+
|
| 267 |
+
Args:
|
| 268 |
+
df: Input DataFrame containing the features.
|
| 269 |
+
|
| 270 |
+
Returns:
|
| 271 |
+
DataFrame with predicted blend properties for 1,2,5,6,7,10.
|
| 272 |
+
"""
|
| 273 |
+
predictions_list = []
|
| 274 |
+
|
| 275 |
+
# Predict individual properties
|
| 276 |
+
predictions_list.append(self.predict_BlendProperty1(df, full=True))
|
| 277 |
+
predictions_list.append(self.predict_BlendProperty2(df, full=True))
|
| 278 |
+
|
| 279 |
+
predictions_list.append(self.predict_BlendProperty5(df, full=True))
|
| 280 |
+
predictions_list.append(self.predict_BlendProperty6(df, full=True))
|
| 281 |
+
predictions_list.append(self.predict_BlendProperty7(df, full=True))
|
| 282 |
+
|
| 283 |
+
predictions_list.append(self.predict_BlendProperty10(df, full=True))
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
# Concatenate the list of single-column DataFrames into a single DataFrame
|
| 287 |
+
predictions_df = pd.concat(predictions_list, axis=1)
|
| 288 |
+
|
| 289 |
+
# Ensure columns are in the desired order
|
| 290 |
+
ordered_cols = [f'BlendProperty{i}' for i in [1,2,5,6,7,10]]
|
| 291 |
+
# Reindex to ensure columns are in order, dropping any not generated (though all should be)
|
| 292 |
+
predictions_df = predictions_df.reindex(columns=ordered_cols)
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
return predictions_df
|
| 296 |
|
| 297 |
|
| 298 |
|
test_app.py
CHANGED
|
@@ -10,7 +10,8 @@ from typing import Optional, Dict, Any
|
|
| 10 |
from datetime import datetime, timedelta
|
| 11 |
import re
|
| 12 |
from pathlib import Path
|
| 13 |
-
|
|
|
|
| 14 |
|
| 15 |
##---- fucntions ------
|
| 16 |
# Load fuel data from CSV (create this file if it doesn't exist)
|
|
@@ -64,7 +65,14 @@ st.markdown("""
|
|
| 64 |
background-color: #f8f5f0;
|
| 65 |
overflow: visible;
|
| 66 |
padding-top: 0
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
}
|
| 69 |
/* Remove unnecessary space at the top */
|
| 70 |
/* Remove any fixed headers */
|
|
@@ -264,7 +272,19 @@ st.markdown("""
|
|
| 264 |
}
|
| 265 |
|
| 266 |
|
| 267 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
|
| 270 |
/* Color scale adjustments */
|
|
@@ -836,8 +856,6 @@ with tabs[0]:
|
|
| 836 |
# Blend Designer Tab
|
| 837 |
# ----------------------------------------------------------------------------------------------------------------------------------------------
|
| 838 |
|
| 839 |
-
from inference import EagleBlendPredictor # Add this import at the top of your main script
|
| 840 |
-
|
| 841 |
# --- Add these new functions to your functions section ---
|
| 842 |
|
| 843 |
@st.cache_data
|
|
@@ -919,8 +937,11 @@ with tabs[1]:
|
|
| 919 |
|
| 920 |
# 4. Run prediction
|
| 921 |
predictor = st.session_state.predictor
|
| 922 |
-
results = predictor.predict_all(df_model.drop(columns=['blend_name']))
|
| 923 |
-
st.session_state.prediction_results = results[0] # Get the first (and only) row of results
|
|
|
|
|
|
|
|
|
|
| 924 |
|
| 925 |
# --- Conditional cost calculation ---
|
| 926 |
# 5. Calculate cost only if all unit costs are provided and greater than zero
|
|
@@ -974,12 +995,74 @@ with tabs[1]:
|
|
| 974 |
with col_header[1]:
|
| 975 |
batch_blend = st.checkbox("Batch Blend Mode", value=False, key="batch_blend_mode")
|
| 976 |
|
|
|
|
| 977 |
if batch_blend:
|
| 978 |
st.subheader("📤 Batch Processing")
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 983 |
else:
|
| 984 |
# --- Manual Blend Designer UI ---
|
| 985 |
all_components_df = get_components_from_db()
|
|
@@ -1202,72 +1285,291 @@ with tabs[1]:
|
|
| 1202 |
# Optimization Engine Tab
|
| 1203 |
# ----------------------------------------------------------------------------------------------------------------------------------------------
|
| 1204 |
|
| 1205 |
-
|
| 1206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1207 |
|
| 1208 |
-
|
| 1209 |
-
|
| 1210 |
-
|
| 1211 |
-
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
x='Cost ($/ton)',
|
| 1219 |
-
y='Performance Score',
|
| 1220 |
-
title="Potential Blend Formulations",
|
| 1221 |
-
color='Performance Score',
|
| 1222 |
-
color_continuous_scale='YlOrBr'
|
| 1223 |
-
)
|
| 1224 |
|
| 1225 |
-
#
|
| 1226 |
-
|
| 1227 |
-
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
|
| 1231 |
-
|
| 1232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1233 |
|
| 1234 |
-
|
| 1235 |
-
|
| 1236 |
-
|
| 1237 |
-
|
| 1238 |
-
|
| 1239 |
-
|
| 1240 |
-
|
| 1241 |
-
|
| 1242 |
-
|
| 1243 |
-
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
|
| 1247 |
-
|
| 1248 |
-
st.
|
| 1249 |
|
| 1250 |
-
#
|
| 1251 |
-
st.
|
| 1252 |
-
|
| 1253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1254 |
|
| 1255 |
-
|
| 1256 |
-
|
| 1257 |
-
|
| 1258 |
-
|
| 1259 |
-
|
| 1260 |
-
|
| 1261 |
-
|
| 1262 |
-
|
| 1263 |
-
|
| 1264 |
-
|
| 1265 |
-
|
| 1266 |
-
|
| 1267 |
-
|
| 1268 |
-
|
| 1269 |
-
|
| 1270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1271 |
|
| 1272 |
# -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
| 1273 |
# Blend Comparison Tab
|
|
@@ -1503,6 +1805,40 @@ with tabs[3]:
|
|
| 1503 |
)
|
| 1504 |
st.plotly_chart(fig_composite, use_container_width=True)
|
| 1505 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1506 |
# ----------------------------------------------------------------------------------------------------------------------------------------------
|
| 1507 |
# Fuel Registry Tab
|
| 1508 |
# ---------------------------------------------------------------------------------------------------------------------------------------------
|
|
@@ -1713,6 +2049,39 @@ with tabs[4]:
|
|
| 1713 |
else:
|
| 1714 |
del st.session_state.blends
|
| 1715 |
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1716 |
|
| 1717 |
|
| 1718 |
# ----------------------------------------------------------------------------------------------------------------------------------------------
|
|
@@ -1776,27 +2145,60 @@ with tabs[5]:
|
|
| 1776 |
</style>
|
| 1777 |
""", unsafe_allow_html=True)
|
| 1778 |
|
| 1779 |
-
# --- Floating "How to Use" Button and Panel ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1780 |
st.markdown("""
|
| 1781 |
-
|
| 1782 |
-
|
| 1783 |
-
|
| 1784 |
-
|
| 1785 |
-
|
| 1786 |
-
|
| 1787 |
-
|
| 1788 |
-
|
| 1789 |
-
|
| 1790 |
-
|
| 1791 |
-
|
| 1792 |
-
|
| 1793 |
-
|
| 1794 |
-
|
| 1795 |
-
|
| 1796 |
-
|
| 1797 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1798 |
</div>
|
| 1799 |
-
</div>
|
| 1800 |
""", unsafe_allow_html=True)
|
| 1801 |
|
| 1802 |
# --- Main Title ---
|
|
|
|
| 10 |
from datetime import datetime, timedelta
|
| 11 |
import re
|
| 12 |
from pathlib import Path
|
| 13 |
+
from predictor import EagleBlendPredictor
|
| 14 |
+
|
| 15 |
|
| 16 |
##---- fucntions ------
|
| 17 |
# Load fuel data from CSV (create this file if it doesn't exist)
|
|
|
|
| 65 |
background-color: #f8f5f0;
|
| 66 |
overflow: visible;
|
| 67 |
padding-top: 0
|
| 68 |
+
|
| 69 |
+
/* --- ADD THIS CSS FOR THE NEW HELP BUTTONS --- */
|
| 70 |
+
#help-toggle-insights:checked ~ .help-panel-insights,
|
| 71 |
+
#help-toggle-registry:checked ~ .help-panel-registry,
|
| 72 |
+
#help-toggle-comparison:checked ~ .help-panel-comparison {
|
| 73 |
+
opacity: 1; visibility: visible; transform: translateY(0);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
}
|
| 77 |
/* Remove unnecessary space at the top */
|
| 78 |
/* Remove any fixed headers */
|
|
|
|
| 272 |
}
|
| 273 |
|
| 274 |
|
| 275 |
+
/* --- Add this CSS class for the spinner --- */
|
| 276 |
+
@keyframes spin {
|
| 277 |
+
0% { transform: rotate(0deg); }
|
| 278 |
+
100% { transform: rotate(360deg); }
|
| 279 |
+
}
|
| 280 |
+
.spinner {
|
| 281 |
+
border: 4px solid rgba(0,0,0,0.1);
|
| 282 |
+
border-left-color: #8B4513;
|
| 283 |
+
border-radius: 50%;
|
| 284 |
+
width: 24px;
|
| 285 |
+
height: 24px;
|
| 286 |
+
animation: spin 1s linear infinite;
|
| 287 |
+
}
|
| 288 |
|
| 289 |
|
| 290 |
/* Color scale adjustments */
|
|
|
|
| 856 |
# Blend Designer Tab
|
| 857 |
# ----------------------------------------------------------------------------------------------------------------------------------------------
|
| 858 |
|
|
|
|
|
|
|
| 859 |
# --- Add these new functions to your functions section ---
|
| 860 |
|
| 861 |
@st.cache_data
|
|
|
|
| 937 |
|
| 938 |
# 4. Run prediction
|
| 939 |
predictor = st.session_state.predictor
|
| 940 |
+
# results = predictor.predict_all(df_model.drop(columns=['blend_name']))
|
| 941 |
+
# st.session_state.prediction_results = results[0] # Get the first (and only) row of results
|
| 942 |
+
# --- FIX: Handles DataFrame output and converts it to an array for single prediction ---
|
| 943 |
+
results_df = predictor.predict_all(df_model.drop(columns=['blend_name']))
|
| 944 |
+
st.session_state.prediction_results = results_df.iloc[0].values
|
| 945 |
|
| 946 |
# --- Conditional cost calculation ---
|
| 947 |
# 5. Calculate cost only if all unit costs are provided and greater than zero
|
|
|
|
| 995 |
with col_header[1]:
|
| 996 |
batch_blend = st.checkbox("Batch Blend Mode", value=False, key="batch_blend_mode")
|
| 997 |
|
| 998 |
+
# --- This is the new, fully functional batch mode block ---
|
| 999 |
if batch_blend:
|
| 1000 |
st.subheader("📤 Batch Processing")
|
| 1001 |
+
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.")
|
| 1002 |
+
|
| 1003 |
+
# Provide a template for download
|
| 1004 |
+
# NOTE: You will need to create a dummy CSV file named 'batch_template.csv'
|
| 1005 |
+
# with the 55 required column headers for this to work.
|
| 1006 |
+
try:
|
| 1007 |
+
with open("assets/batch_template.csv", "rb") as f:
|
| 1008 |
+
st.download_button(
|
| 1009 |
+
label="📥 Download Batch Template (CSV)",
|
| 1010 |
+
data=f,
|
| 1011 |
+
file_name="batch_template.csv",
|
| 1012 |
+
mime="text/csv"
|
| 1013 |
+
)
|
| 1014 |
+
except FileNotFoundError:
|
| 1015 |
+
st.warning("Batch template file not found. Please create 'assets/batch_template.csv'.")
|
| 1016 |
+
|
| 1017 |
+
|
| 1018 |
+
uploaded_file = st.file_uploader("Upload your CSV file", type=["csv"], key="batch_upload")
|
| 1019 |
+
|
| 1020 |
+
if uploaded_file is not None:
|
| 1021 |
+
try:
|
| 1022 |
+
input_df = pd.read_csv(uploaded_file)
|
| 1023 |
+
st.markdown("##### Uploaded Data Preview")
|
| 1024 |
+
st.dataframe(input_df.head())
|
| 1025 |
+
|
| 1026 |
+
if st.button("🧪 Run Batch Prediction", use_container_width=True, type="primary"):
|
| 1027 |
+
# Basic validation: check for at least the fraction columns
|
| 1028 |
+
required_cols = [f'Component{i+1}_fraction' for i in range(5)]
|
| 1029 |
+
if not all(col in input_df.columns for col in required_cols):
|
| 1030 |
+
st.error(f"Invalid file format. The uploaded CSV is missing one or more required columns like: {', '.join(required_cols)}")
|
| 1031 |
+
else:
|
| 1032 |
+
with st.spinner("Running batch prediction... This may take a moment."):
|
| 1033 |
+
# Run prediction on the entire DataFrame
|
| 1034 |
+
predictor = st.session_state.predictor
|
| 1035 |
+
results_df = predictor.predict_all(input_df)
|
| 1036 |
+
|
| 1037 |
+
# Combine original data with the results
|
| 1038 |
+
# Ensure column names for results are clear
|
| 1039 |
+
results_df.columns = [f"BlendProperty{i+1}" for i in range(results_df.shape[1])]
|
| 1040 |
+
|
| 1041 |
+
# Combine input and output dataframes
|
| 1042 |
+
final_df = pd.concat([input_df.reset_index(drop=True), results_df.reset_index(drop=True)], axis=1)
|
| 1043 |
+
|
| 1044 |
+
st.session_state['batch_results'] = final_df
|
| 1045 |
+
st.success("Batch prediction complete!")
|
| 1046 |
+
|
| 1047 |
+
except Exception as e:
|
| 1048 |
+
st.error(f"An error occurred while processing the file: {e}")
|
| 1049 |
+
|
| 1050 |
+
# Display results and download button if they exist in the session state
|
| 1051 |
+
if 'batch_results' in st.session_state:
|
| 1052 |
+
st.markdown("---")
|
| 1053 |
+
st.subheader("✅ Batch Prediction Results")
|
| 1054 |
+
|
| 1055 |
+
results_to_show = st.session_state['batch_results']
|
| 1056 |
+
st.dataframe(results_to_show)
|
| 1057 |
+
|
| 1058 |
+
csv_data = results_to_show.to_csv(index=False).encode('utf-8')
|
| 1059 |
+
st.download_button(
|
| 1060 |
+
label="📥 Download Full Results (CSV)",
|
| 1061 |
+
data=csv_data,
|
| 1062 |
+
file_name="batch_prediction_results.csv",
|
| 1063 |
+
mime="text/csv",
|
| 1064 |
+
use_container_width=True
|
| 1065 |
+
)
|
| 1066 |
else:
|
| 1067 |
# --- Manual Blend Designer UI ---
|
| 1068 |
all_components_df = get_components_from_db()
|
|
|
|
| 1285 |
# Optimization Engine Tab
|
| 1286 |
# ----------------------------------------------------------------------------------------------------------------------------------------------
|
| 1287 |
|
| 1288 |
+
import time # Add this import to the top of your script
|
| 1289 |
+
|
| 1290 |
+
# --- Add this new function to your functions section ---
|
| 1291 |
+
def dummy_optimization_function(targets, fixed_targets, components_data):
|
| 1292 |
+
"""
|
| 1293 |
+
Placeholder for your actual optimization algorithm.
|
| 1294 |
+
This function simulates a multi-objective optimization.
|
| 1295 |
|
| 1296 |
+
Returns:
|
| 1297 |
+
A list of dictionaries, where each dictionary represents a solution.
|
| 1298 |
+
"""
|
| 1299 |
+
print("--- Running Dummy Optimization ---")
|
| 1300 |
+
print("Targets:", targets)
|
| 1301 |
+
print("Fixed Targets:", fixed_targets)
|
| 1302 |
+
print("---------------------------------")
|
| 1303 |
|
| 1304 |
+
# Simulate a process that takes a few seconds
|
| 1305 |
+
time.sleep(3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1306 |
|
| 1307 |
+
# Generate 3 dummy solutions
|
| 1308 |
+
solutions = []
|
| 1309 |
+
for i in range(3):
|
| 1310 |
+
# Create slightly different results for each solution
|
| 1311 |
+
base_frac = 0.2 + (i * 0.05)
|
| 1312 |
+
fractions = np.random.rand(5)
|
| 1313 |
+
fractions = fractions / fractions.sum() # Normalize to sum to 1
|
| 1314 |
+
|
| 1315 |
+
blend_properties = [val + np.random.uniform(-0.5, 0.5) for val in targets.values()]
|
| 1316 |
+
|
| 1317 |
+
# Ensure fixed targets are met in the dummy result
|
| 1318 |
+
for prop, val in fixed_targets.items():
|
| 1319 |
+
prop_index = int(prop.replace('Property', '')) - 1
|
| 1320 |
+
blend_properties[prop_index] = val
|
| 1321 |
+
|
| 1322 |
+
solution = {
|
| 1323 |
+
"component_fractions": fractions,
|
| 1324 |
+
"blend_properties": np.array(blend_properties),
|
| 1325 |
+
"optimized_cost": 150.0 - (i * 10),
|
| 1326 |
+
"error": 0.05 + (i * 0.02) # Dummy error for the Pareto plot
|
| 1327 |
+
}
|
| 1328 |
+
solutions.append(solution)
|
| 1329 |
+
|
| 1330 |
+
return solutions
|
| 1331 |
|
| 1332 |
+
|
| 1333 |
+
with tabs[2]:
|
| 1334 |
+
st.subheader("⚙️ Optimization Engine")
|
| 1335 |
+
st.markdown("Define your property goals, select base components, and run the optimizer to find the ideal blend recipe.")
|
| 1336 |
+
|
| 1337 |
+
# --- State Initialization ---
|
| 1338 |
+
if 'optimization_running' not in st.session_state:
|
| 1339 |
+
st.session_state.optimization_running = False
|
| 1340 |
+
if 'optimization_results' not in st.session_state:
|
| 1341 |
+
st.session_state.optimization_results = None
|
| 1342 |
+
if 'optimization_time' not in st.session_state:
|
| 1343 |
+
st.session_state.optimization_time = 0.0
|
| 1344 |
+
|
| 1345 |
+
# --- Optimization Goals ---
|
| 1346 |
+
st.markdown("#### 1. Define Optimization Goals")
|
| 1347 |
|
| 1348 |
+
# Using a container to group the goal inputs
|
| 1349 |
+
with st.container(border=True):
|
| 1350 |
+
cols_row1 = st.columns(5)
|
| 1351 |
+
cols_row2 = st.columns(5)
|
| 1352 |
+
|
| 1353 |
+
for i in range(1, 11):
|
| 1354 |
+
col = cols_row1[(i-1)] if i <= 5 else cols_row2[(i-6)]
|
| 1355 |
+
with col:
|
| 1356 |
+
st.number_input(f"Property {i}", key=f"opt_target_{i}", value=0.0, step=0.01, format="%.4f")
|
| 1357 |
+
st.toggle("Fix Target", key=f"opt_fix_{i}", help=f"Toggle on to make Property {i} a fixed constraint.")
|
| 1358 |
+
|
| 1359 |
+
# --- Component Selection (Copied and Adapted) ---
|
| 1360 |
+
st.markdown("#### 2. Select Initial Components")
|
| 1361 |
+
all_components_df_opt = get_components_from_db() # Use a different variable to avoid conflicts
|
| 1362 |
|
| 1363 |
+
main_cols = st.columns(2)
|
| 1364 |
+
with main_cols[0]: # Left side for first 3 components
|
| 1365 |
+
for i in range(3):
|
| 1366 |
+
with st.expander(f"**Component {i+1}**", expanded=(i==0)):
|
| 1367 |
+
# Auto-population and input fields logic (reused from Blend Designer)
|
| 1368 |
+
# Note: Keys are prefixed with 'opt_' to ensure they are unique to this tab
|
| 1369 |
+
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"
|
| 1370 |
+
|
| 1371 |
+
# Auto-population logic...
|
| 1372 |
+
if select_key in st.session_state and st.session_state[select_key] != "---":
|
| 1373 |
+
selected_name = st.session_state[select_key]
|
| 1374 |
+
comp_data = all_components_df_opt[all_components_df_opt['component_name'] == selected_name].iloc[0]
|
| 1375 |
+
st.session_state[name_key] = comp_data['component_name']
|
| 1376 |
+
st.session_state[frac_key] = comp_data.get('component_fraction', 0.2)
|
| 1377 |
+
cost_val = comp_data.get('unit_cost', 0.0)
|
| 1378 |
+
st.session_state[cost_key] = 0.0 if pd.isna(cost_val) else float(cost_val)
|
| 1379 |
+
for j in range(1, 11):
|
| 1380 |
+
st.session_state[f"opt_c{i}_prop{j}"] = comp_data.get(f'property{j}', 0.0)
|
| 1381 |
+
st.session_state[select_key] = "---"
|
| 1382 |
+
|
| 1383 |
+
# UI for component
|
| 1384 |
+
component_options = ["---"] + all_components_df_opt['component_name'].tolist()
|
| 1385 |
+
st.selectbox("Load from Registry", options=component_options, key=select_key)
|
| 1386 |
+
c1, c2, c3 = st.columns([1.5, 2, 2])
|
| 1387 |
+
with c1:
|
| 1388 |
+
st.text_input("Component Name", key=name_key)
|
| 1389 |
+
st.number_input("Unit Cost ($)", min_value=0.0, step=0.01, key=cost_key, format="%.2f")
|
| 1390 |
+
with c2:
|
| 1391 |
+
for j in range(1, 6): st.number_input(f"Property {j}", key=f"opt_c{i}_prop{j}", format="%.4f")
|
| 1392 |
+
with c3:
|
| 1393 |
+
for j in range(6, 11): st.number_input(f"Property {j}", key=f"opt_c{i}_prop{j}", format="%.4f")
|
| 1394 |
+
|
| 1395 |
+
with main_cols[1]: # Right side for last 2 components and controls
|
| 1396 |
+
for i in range(3, 5):
|
| 1397 |
+
with st.expander(f"**Component {i+1}**", expanded=False):
|
| 1398 |
+
# Auto-population and input fields logic...
|
| 1399 |
+
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"
|
| 1400 |
+
if select_key in st.session_state and st.session_state[select_key] != "---":
|
| 1401 |
+
selected_name = st.session_state[select_key]
|
| 1402 |
+
comp_data = all_components_df_opt[all_components_df_opt['component_name'] == selected_name].iloc[0]
|
| 1403 |
+
st.session_state[name_key] = comp_data['component_name']
|
| 1404 |
+
st.session_state[frac_key] = comp_data.get('component_fraction', 0.2)
|
| 1405 |
+
cost_val = comp_data.get('unit_cost', 0.0)
|
| 1406 |
+
st.session_state[cost_key] = 0.0 if pd.isna(cost_val) else float(cost_val)
|
| 1407 |
+
for j in range(1, 11):
|
| 1408 |
+
st.session_state[f"opt_c{i}_prop{j}"] = comp_data.get(f'property{j}', 0.0)
|
| 1409 |
+
st.session_state[select_key] = "---"
|
| 1410 |
+
component_options = ["---"] + all_components_df_opt['component_name'].tolist()
|
| 1411 |
+
st.selectbox("Load from Registry", options=component_options, key=select_key)
|
| 1412 |
+
c1, c2, c3 = st.columns([1.5, 2, 2])
|
| 1413 |
+
with c1:
|
| 1414 |
+
st.text_input("Component Name", key=name_key)
|
| 1415 |
+
st.number_input("Unit Cost ($)", min_value=0.0, step=0.01, key=cost_key, format="%.2f")
|
| 1416 |
+
with c2:
|
| 1417 |
+
for j in range(1, 6): st.number_input(f"Property {j}", key=f"opt_c{i}_prop{j}", format="%.4f")
|
| 1418 |
+
with c3:
|
| 1419 |
+
for j in range(6, 11): st.number_input(f"Property {j}", key=f"opt_c{i}_prop{j}", format="%.4f")
|
| 1420 |
+
|
| 1421 |
+
# --- Optimization Controls ---
|
| 1422 |
+
with st.container(border=True):
|
| 1423 |
+
st.markdown("##### 3. Configure & Run")
|
| 1424 |
+
st.checkbox("Include Cost in Optimization", value=True, key="opt_include_cost")
|
| 1425 |
+
|
| 1426 |
+
# Run button and spinner logic
|
| 1427 |
+
run_button_col, spinner_col = st.columns([3, 1])
|
| 1428 |
+
with run_button_col:
|
| 1429 |
+
if st.button("🚀 Run Optimization", use_container_width=True, type="primary", disabled=st.session_state.optimization_running):
|
| 1430 |
+
st.session_state.optimization_running = True
|
| 1431 |
+
start_time = time.time()
|
| 1432 |
+
|
| 1433 |
+
# Gather data for the optimization function
|
| 1434 |
+
targets = {f"Property{i}": st.session_state[f"opt_target_{i}"] for i in range(1, 11)}
|
| 1435 |
+
fixed_targets = {f"Property{i}": targets[f"Property{i}"] for i in range(1, 11) if st.session_state[f"opt_fix_{i}"]}
|
| 1436 |
+
components_data = [] # You would gather component data similarly if your function needs it
|
| 1437 |
+
|
| 1438 |
+
# Call the (dummy) optimization function
|
| 1439 |
+
st.session_state.optimization_results = dummy_optimization_function(targets, fixed_targets, components_data)
|
| 1440 |
+
st.session_state.optimization_time = time.time() - start_time
|
| 1441 |
+
st.session_state.optimization_running = False
|
| 1442 |
+
st.rerun() # Rerun to display results
|
| 1443 |
+
|
| 1444 |
+
with spinner_col:
|
| 1445 |
+
if st.session_state.optimization_running:
|
| 1446 |
+
st.markdown('<div class="spinner"></div>', unsafe_allow_html=True)
|
| 1447 |
+
|
| 1448 |
+
if st.session_state.optimization_time > 0:
|
| 1449 |
+
st.success(f"Optimization complete in {st.session_state.optimization_time:.2f} seconds.")
|
| 1450 |
+
|
| 1451 |
+
# --- Results Section ---
|
| 1452 |
+
if st.session_state.optimization_results:
|
| 1453 |
+
st.markdown('<hr class="custom-divider">', unsafe_allow_html=True)
|
| 1454 |
+
st.subheader("🏆 Optimization Results")
|
| 1455 |
+
|
| 1456 |
+
results = st.session_state.optimization_results
|
| 1457 |
+
|
| 1458 |
+
# Dropdown to select which result to view
|
| 1459 |
+
result_options = {i: f"Solution {i+1}" for i in range(len(results))}
|
| 1460 |
+
selected_idx = st.selectbox("View Solution", options=list(result_options.keys()), format_func=lambda x: result_options[x])
|
| 1461 |
+
|
| 1462 |
+
selected_solution = results[selected_idx]
|
| 1463 |
+
|
| 1464 |
+
# Display best fractions and properties
|
| 1465 |
+
res_cols = st.columns([3, 2])
|
| 1466 |
+
with res_cols[0]:
|
| 1467 |
+
st.markdown("##### Optimal Component Fractions")
|
| 1468 |
+
frac_cols = st.columns(5)
|
| 1469 |
+
for i, frac in enumerate(selected_solution["component_fractions"]):
|
| 1470 |
+
with frac_cols[i]:
|
| 1471 |
+
comp_name = st.session_state.get(f"opt_c{i}_name", f"Component {i+1}")
|
| 1472 |
+
st.markdown(f"""
|
| 1473 |
+
<div class="metric-card" style="padding: 0.8rem;">
|
| 1474 |
+
<div class="metric-label" style="font-size: 0.8rem;">{comp_name}</div>
|
| 1475 |
+
<div class="metric-value" style="font-size: 1.5rem;">{frac*100:.2f}%</div>
|
| 1476 |
+
</div>
|
| 1477 |
+
""", unsafe_allow_html=True)
|
| 1478 |
+
|
| 1479 |
+
# --- FIX: New, readable KPI cards for blend properties ---
|
| 1480 |
+
with res_cols[1]:
|
| 1481 |
+
st.markdown("##### Resulting Blend Properties")
|
| 1482 |
+
prop_kpi_cols = st.columns(5)
|
| 1483 |
+
for i, prop_val in enumerate(selected_solution["blend_properties"]):
|
| 1484 |
+
col = prop_kpi_cols[i % 5]
|
| 1485 |
+
with col:
|
| 1486 |
+
st.markdown(f"""
|
| 1487 |
+
<div class="metric-card" style="margin-bottom: 10px; padding: 0.5rem;">
|
| 1488 |
+
<div class="metric-label" style="font-size: 0.7rem;">Property {i+1}</div>
|
| 1489 |
+
<div class="metric-value" style="font-size: 1.1rem;">{prop_val:.4f}</div>
|
| 1490 |
+
</div>
|
| 1491 |
+
""", unsafe_allow_html=True)
|
| 1492 |
+
|
| 1493 |
+
# Expander for full results table
|
| 1494 |
+
with st.expander("Show Full Results Table"):
|
| 1495 |
+
table_data = []
|
| 1496 |
+
for i in range(5):
|
| 1497 |
+
row = {
|
| 1498 |
+
"Composition": st.session_state.get(f"opt_c{i}_name", f"C{i+1}"),
|
| 1499 |
+
"Fraction": selected_solution["component_fractions"][i],
|
| 1500 |
+
"Unit Cost": st.session_state.get(f"opt_c{i}_cost", 0.0)
|
| 1501 |
+
}
|
| 1502 |
+
for j in range(1, 11):
|
| 1503 |
+
row[f"Property {j}"] = st.session_state.get(f"opt_c{i}_prop{j}", 0.0)
|
| 1504 |
+
table_data.append(row)
|
| 1505 |
+
|
| 1506 |
+
# Add blend row
|
| 1507 |
+
blend_row = {"Composition": "Optimized Blend", "Fraction": 1.0, "Unit Cost": selected_solution["optimized_cost"]}
|
| 1508 |
+
for i, prop in enumerate(selected_solution["blend_properties"]):
|
| 1509 |
+
blend_row[f"Property {i+1}"] = prop
|
| 1510 |
+
table_data.append(blend_row)
|
| 1511 |
+
|
| 1512 |
+
st.dataframe(pd.DataFrame(table_data), use_container_width=True)
|
| 1513 |
+
|
| 1514 |
+
# Pareto Plot and Save Section
|
| 1515 |
+
pareto_col, save_col = st.columns([2, 1])
|
| 1516 |
+
with pareto_col:
|
| 1517 |
+
st.markdown("##### Pareto Front: Cost vs. Error")
|
| 1518 |
+
pareto_df = pd.DataFrame({
|
| 1519 |
+
'Cost': [r['optimized_cost'] for r in results],
|
| 1520 |
+
'Error': [r['error'] for r in results],
|
| 1521 |
+
'Solution': [f'Sol {i+1}' for i in range(len(results))]
|
| 1522 |
+
})
|
| 1523 |
+
# --- FIX: Inverted the axes to show Error vs. Cost ---
|
| 1524 |
+
fig_pareto = px.scatter(
|
| 1525 |
+
pareto_df, x='Error', y='Cost', text='Solution', title="<b>Pareto Front: Error vs. Cost</b>"
|
| 1526 |
+
)
|
| 1527 |
+
fig_pareto.update_traces(textposition='top center', marker=dict(size=12, color='#8B4513'))
|
| 1528 |
+
st.plotly_chart(fig_pareto, use_container_width=True)
|
| 1529 |
+
|
| 1530 |
+
with save_col:
|
| 1531 |
+
st.markdown("##### Save Result")
|
| 1532 |
+
st.text_input("Save as Blend Name", value=f"Optimized_Blend_{selected_idx+1}", key="opt_save_name")
|
| 1533 |
+
if st.button("💾 Save to Database", use_container_width=True):
|
| 1534 |
+
st.info("Save functionality can be implemented here.") # Placeholder for save logic
|
| 1535 |
+
|
| 1536 |
+
# Placeholder for download button logic
|
| 1537 |
+
st.download_button("📥 Download All Solutions (CSV)", data="dummy_csv_data", file_name="optimization_results.csv", use_container_width=True)
|
| 1538 |
+
|
| 1539 |
+
# --- Floating Help Button ---
|
| 1540 |
+
# (Using a different key to avoid conflict with other tabs)
|
| 1541 |
+
# --- FIX: Complete working version of the help button ---
|
| 1542 |
+
st.markdown("""
|
| 1543 |
+
<style>
|
| 1544 |
+
#help-toggle-optimizer { display: none; }
|
| 1545 |
+
#help-toggle-optimizer:checked ~ .help-panel-optimizer {
|
| 1546 |
+
opacity: 1; visibility: visible; transform: translateY(0);
|
| 1547 |
+
}
|
| 1548 |
+
.help-panel-optimizer {
|
| 1549 |
+
position:fixed; right:25px; bottom:100px; z-index:9998;
|
| 1550 |
+
width:520px; max-height:70vh; overflow-y:auto;
|
| 1551 |
+
background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
|
| 1552 |
+
border:1px solid #CFB53B; border-radius:12px; padding:20px;
|
| 1553 |
+
box-shadow:0 14px 34px rgba(0,0,0,0.22);
|
| 1554 |
+
color:#4a2f1f; transform: translateY(12px); opacity:0;
|
| 1555 |
+
visibility:hidden; transition: all .22s ease-in-out;
|
| 1556 |
+
}
|
| 1557 |
+
</style>
|
| 1558 |
+
<input id="help-toggle-optimizer" type="checkbox" />
|
| 1559 |
+
<label for="help-toggle-optimizer" class="help-button">💬 Help</label>
|
| 1560 |
+
<div class="help-panel help-panel-optimizer"> <div class="head">
|
| 1561 |
+
<div class="title">How to Use the Optimizer</div>
|
| 1562 |
+
<label for="help-toggle-optimizer" class="help-close">Close</label>
|
| 1563 |
+
</div>
|
| 1564 |
+
<div class="help-body">
|
| 1565 |
+
<p><b>1. Define Goals:</b> Enter your desired target values for each of the 10 blend properties. Use the 'Fix Target' toggle for any property that must be met exactly.</p>
|
| 1566 |
+
<p><b>2. Select Components:</b> Choose up to 5 base components. You can load them from the registry to auto-fill their data or enter them manually.</p>
|
| 1567 |
+
<p><b>3. Configure & Run:</b> Decide if cost should be a factor in the optimization, then click 'Run Optimization'. A spinner will appear while the process runs.</p>
|
| 1568 |
+
<p><b>4. Analyze Results:</b> After completion, the best solution is shown by default. You can view other potential solutions from the dropdown. The results include optimal component fractions and the final blend properties.</p>
|
| 1569 |
+
<p><b>5. Save & Download:</b> Give your chosen solution a name and save it to the blends database for future use in the Comparison tab.</p>
|
| 1570 |
+
</div>
|
| 1571 |
+
</div>
|
| 1572 |
+
""", unsafe_allow_html=True)
|
| 1573 |
|
| 1574 |
# -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
| 1575 |
# Blend Comparison Tab
|
|
|
|
| 1805 |
)
|
| 1806 |
st.plotly_chart(fig_composite, use_container_width=True)
|
| 1807 |
|
| 1808 |
+
# --- ADD: Floating Help Button for Blend Comparison ---
|
| 1809 |
+
st.markdown("""
|
| 1810 |
+
<style>
|
| 1811 |
+
#help-toggle-comparison { display: none; }
|
| 1812 |
+
#help-toggle-comparison:checked ~ .help-panel-comparison {
|
| 1813 |
+
opacity: 1; visibility: visible; transform: translateY(0);
|
| 1814 |
+
}
|
| 1815 |
+
.help-panel-comparison {
|
| 1816 |
+
position:fixed; right:25px; bottom:100px; z-index:9998;
|
| 1817 |
+
width:520px; max-height:70vh; overflow-y:auto;
|
| 1818 |
+
background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
|
| 1819 |
+
border:1px solid #CFB53B; border-radius:12px; padding:20px;
|
| 1820 |
+
box-shadow:0 14px 34px rgba(0,0,0,0.22);
|
| 1821 |
+
color:#4a2f1f; transform: translateY(12px); opacity:0;
|
| 1822 |
+
visibility:hidden; transition: all .22s ease-in-out;
|
| 1823 |
+
}
|
| 1824 |
+
</style>
|
| 1825 |
+
<input id="help-toggle-comparison" type="checkbox" />
|
| 1826 |
+
<label for="help-toggle-comparison" class="help-button">💬 Help</label>
|
| 1827 |
+
<div class="help-panel help-panel-comparison">
|
| 1828 |
+
<div class="head">
|
| 1829 |
+
<div class="title">Using the Blend Comparison Tool</div>
|
| 1830 |
+
<label for="help-toggle-comparison" class="help-close">Close</label>
|
| 1831 |
+
</div>
|
| 1832 |
+
<div class="help-body">
|
| 1833 |
+
<p>This tab allows you to perform a side-by-side analysis of up to three saved blends.</p>
|
| 1834 |
+
<p><b>1. Select Scenarios:</b> Use the three dropdown menus at the top to select the saved blends you wish to compare.</p>
|
| 1835 |
+
<p><b>2. Review Overviews:</b> Key information for each selected blend, including its composition and final properties, will be displayed in summary cards.</p>
|
| 1836 |
+
<p><b>3. Analyze Charts:</b> The charts provide a deep dive into how the blends compare on cost, property profiles, quality, and composition.</p>
|
| 1837 |
+
<p><b>4. Export:</b> Click the 'Export to PDF' button to generate a downloadable report containing all the charts and data for your selected comparison.</p>
|
| 1838 |
+
</div>
|
| 1839 |
+
</div>
|
| 1840 |
+
""", unsafe_allow_html=True)
|
| 1841 |
+
|
| 1842 |
# ----------------------------------------------------------------------------------------------------------------------------------------------
|
| 1843 |
# Fuel Registry Tab
|
| 1844 |
# ---------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
| 2049 |
else:
|
| 2050 |
del st.session_state.blends
|
| 2051 |
st.rerun()
|
| 2052 |
+
|
| 2053 |
+
# --- ADD: Floating Help Button for Fuel Registry ---
|
| 2054 |
+
st.markdown("""
|
| 2055 |
+
<style>
|
| 2056 |
+
#help-toggle-registry { display: none; }
|
| 2057 |
+
#help-toggle-registry:checked ~ .help-panel-registry {
|
| 2058 |
+
opacity: 1; visibility: visible; transform: translateY(0);
|
| 2059 |
+
}
|
| 2060 |
+
.help-panel-registry {
|
| 2061 |
+
position:fixed; right:25px; bottom:100px; z-index:9998;
|
| 2062 |
+
width:520px; max-height:70vh; overflow-y:auto;
|
| 2063 |
+
background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
|
| 2064 |
+
border:1px solid #CFB53B; border-radius:12px; padding:20px;
|
| 2065 |
+
box-shadow:0 14px 34px rgba(0,0,0,0.22);
|
| 2066 |
+
color:#4a2f1f; transform: translateY(12px); opacity:0;
|
| 2067 |
+
visibility:hidden; transition: all .22s ease-in-out;
|
| 2068 |
+
}
|
| 2069 |
+
</style>
|
| 2070 |
+
<input id="help-toggle-registry" type="checkbox" />
|
| 2071 |
+
<label for="help-toggle-registry" class="help-button">💬 Help</label>
|
| 2072 |
+
<div class="help-panel help-panel-registry">
|
| 2073 |
+
<div class="head">
|
| 2074 |
+
<div class="title">Using the Fuel Registry</div>
|
| 2075 |
+
<label for="help-toggle-registry" class="help-close">Close</label>
|
| 2076 |
+
</div>
|
| 2077 |
+
<div class="help-body">
|
| 2078 |
+
<p>This tab is your central database for managing all blend components and saved blends.</p>
|
| 2079 |
+
<p><b>1. Add Components/Blends:</b> You can add a single component manually using the form or upload a CSV file for batch additions of components or blends. Download the templates to ensure your file format is correct.</p>
|
| 2080 |
+
<p><b>2. View & Manage Data:</b> Use the dropdown to switch between viewing 'Components' and 'Blends'. The table shows all saved records.</p>
|
| 2081 |
+
<p><b>3. Search & Delete:</b> Use the search bar to filter the table. To delete records, check the 'Select' box next to the desired rows and click the 'Delete Selected' button that appears.</p>
|
| 2082 |
+
</div>
|
| 2083 |
+
</div>
|
| 2084 |
+
""", unsafe_allow_html=True)
|
| 2085 |
|
| 2086 |
|
| 2087 |
# ----------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
| 2145 |
</style>
|
| 2146 |
""", unsafe_allow_html=True)
|
| 2147 |
|
| 2148 |
+
# # --- Floating "How to Use" Button and Panel ---
|
| 2149 |
+
# st.markdown("""
|
| 2150 |
+
# <input id="help-toggle" type="checkbox" />
|
| 2151 |
+
# <label for="help-toggle" class="help-button">💬 Help</label>
|
| 2152 |
+
|
| 2153 |
+
# <div class="help-panel" aria-hidden="true">
|
| 2154 |
+
# <div class="head">
|
| 2155 |
+
# <div class="title">Interpreting Model Insights</div>
|
| 2156 |
+
# <label for="help-toggle" class="help-close">Close</label>
|
| 2157 |
+
# </div>
|
| 2158 |
+
# <div class="help-body">
|
| 2159 |
+
# <p><b>KPI Cards:</b> These four cards give you a quick summary of the model's overall health.</p>
|
| 2160 |
+
# <ul>
|
| 2161 |
+
# <li><b>Overall R² Score:</b> Think of this as the model's accuracy grade. A score of 92.4% means the model's predictions are highly accurate.</li>
|
| 2162 |
+
# <li><b>MSE (Mean Squared Error):</b> This measures the average size of the model's mistakes. A smaller number is better.</li>
|
| 2163 |
+
# <li><b>MAPE (Mean Absolute % Error):</b> This tells you the average error in percentage terms. A value of 0.112 means predictions are off by about 11.2% on average.</li>
|
| 2164 |
+
# </ul>
|
| 2165 |
+
# <p><b>R² Score by Blend Property Chart:</b> This chart shows how well the model predicts each specific property.</p>
|
| 2166 |
+
# <p>A <b>longer bar</b> means the model is very good at predicting that property. A <b>shorter bar</b> indicates a property that is harder for the model to predict accurately. This helps you trust predictions for some properties more than others.</p>
|
| 2167 |
+
# </div>
|
| 2168 |
+
# </div>
|
| 2169 |
+
# """, unsafe_allow_html=True)
|
| 2170 |
+
|
| 2171 |
+
# --- FIX: Complete working version of the help button ---
|
| 2172 |
+
# --- FIX: Complete working version of the help button ---
|
| 2173 |
st.markdown("""
|
| 2174 |
+
<style>
|
| 2175 |
+
/* Styles for the help panel and button */
|
| 2176 |
+
#help-toggle-insights { display: none; }
|
| 2177 |
+
#help-toggle-insights:checked ~ .help-panel-insights {
|
| 2178 |
+
opacity: 1; visibility: visible; transform: translateY(0);
|
| 2179 |
+
}
|
| 2180 |
+
.help-panel-insights {
|
| 2181 |
+
position:fixed; right:25px; bottom:100px; z-index:9998;
|
| 2182 |
+
width:520px; max-height:70vh; overflow-y:auto;
|
| 2183 |
+
background: linear-gradient(135deg, #FFFDF5 0%, #F8EAD9 100%);
|
| 2184 |
+
border:1px solid #CFB53B; border-radius:12px; padding:20px;
|
| 2185 |
+
box-shadow:0 14px 34px rgba(0,0,0,0.22);
|
| 2186 |
+
color:#4a2f1f; transform: translateY(12px); opacity:0;
|
| 2187 |
+
visibility:hidden; transition: all .22s ease-in-out;
|
| 2188 |
+
}
|
| 2189 |
+
</style>
|
| 2190 |
+
<input id="help-toggle-insights" type="checkbox" />
|
| 2191 |
+
<label for="help-toggle-insights" class="help-button">💬 Help</label>
|
| 2192 |
+
<div class="help-panel help-panel-insights">
|
| 2193 |
+
<div class="head">
|
| 2194 |
+
<div class="title">Interpreting Model Insights</div>
|
| 2195 |
+
<label for="help-toggle-insights" class="help-close">Close</label>
|
| 2196 |
+
</div>
|
| 2197 |
+
<div class="help-body">
|
| 2198 |
+
<p><b>KPI Cards:</b> These cards give a quick summary of the model's health. <b>R² Score</b> is its accuracy grade, while <b>MSE</b> and <b>MAPE</b> measure the average size of its errors.</p>
|
| 2199 |
+
<p><b>R² Score by Blend Property Chart:</b> This chart shows how well the model predicts each specific property. A longer bar means the model is very good at predicting that property.</p>
|
| 2200 |
+
</div>
|
| 2201 |
</div>
|
|
|
|
| 2202 |
""", unsafe_allow_html=True)
|
| 2203 |
|
| 2204 |
# --- Main Title ---
|