Spaces:
Runtime error
Runtime error
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +195 -183
src/streamlit_app.py
CHANGED
|
@@ -4,115 +4,115 @@ import numpy as np
|
|
| 4 |
from sklearn.preprocessing import StandardScaler
|
| 5 |
from sklearn.neighbors import KNeighborsRegressor
|
| 6 |
|
| 7 |
-
# ---------------------------
|
| 8 |
-
# 1.
|
| 9 |
-
# ---------------------------
|
| 10 |
-
st.set_page_config(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
|
|
|
| 12 |
st.markdown("""
|
| 13 |
-
<style>
|
| 14 |
-
/* Main Background - Apple Light
|
| 15 |
.stApp {
|
| 16 |
-
background-color: #
|
| 17 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 18 |
}
|
| 19 |
|
| 20 |
-
/* Sidebar
|
| 21 |
section[data-testid="stSidebar"] {
|
| 22 |
-
background-color: #
|
| 23 |
-
border-right: 1px solid #
|
| 24 |
}
|
| 25 |
|
| 26 |
-
/*
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
border-radius: 18px;
|
| 30 |
-
padding: 20px;
|
| 31 |
-
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
|
| 32 |
-
border: 1px solid #e5e5ea;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
/* Input Widgets - Rounded pills */
|
| 36 |
-
.stSelectbox > div > div {
|
| 37 |
-
border-radius: 12px;
|
| 38 |
-
border: 1px solid #d1d1d6;
|
| 39 |
-
}
|
| 40 |
-
.stNumberInput > div > div > input {
|
| 41 |
-
border-radius: 12px;
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
/* Metrics Styling */
|
| 45 |
-
div[data-testid="stMetricValue"] {
|
| 46 |
font-weight: 600;
|
| 47 |
-
color: #1d1d1f;
|
| 48 |
}
|
| 49 |
|
| 50 |
-
/*
|
| 51 |
-
|
| 52 |
-
color: #
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
|
| 57 |
-
/* Custom
|
| 58 |
-
.
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
| 61 |
margin-bottom: 20px;
|
| 62 |
}
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
.
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
|
|
|
|
|
|
|
|
|
|
| 74 |
@st.cache_resource
|
| 75 |
-
def
|
| 76 |
np.random.seed(42)
|
| 77 |
-
n = 1000 # More data for better accuracy with V12s
|
| 78 |
data = []
|
| 79 |
|
| 80 |
for _ in range(n):
|
| 81 |
# Expanded Engine Options
|
| 82 |
-
|
| 83 |
-
cyl = np.random.choice([4, 6, 8, 10, 12])
|
| 84 |
|
| 85 |
-
#
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
-
#
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
induction = np.random.choice([0, 1, 2, 3]) # None/Turbo/Twin-Turbo/Super
|
| 92 |
-
|
| 93 |
-
nitrous = np.random.choice([0, 1]) # No/Yes (50 shot equivalent)
|
| 94 |
fuel = np.random.choice([0, 1, 2, 3]) # 87/91/93/E85
|
| 95 |
-
tune = np.random.choice([0, 1, 2])
|
| 96 |
|
| 97 |
-
#
|
| 98 |
-
|
| 99 |
-
(intake *
|
| 100 |
-
(
|
| 101 |
-
(
|
| 102 |
-
(
|
| 103 |
-
(
|
| 104 |
-
(tune *
|
| 105 |
-
(fuel *
|
| 106 |
np.random.uniform(-5, 5)
|
| 107 |
)
|
| 108 |
|
| 109 |
-
# Diminishing returns for
|
| 110 |
-
if
|
| 111 |
-
|
| 112 |
|
| 113 |
-
data.append([engine, cyl, base_hp, intake, exhaust, induction,
|
| 114 |
|
| 115 |
-
columns = ["engine", "cyl", "base_hp", "intake", "
|
| 116 |
df = pd.DataFrame(data, columns=columns)
|
| 117 |
|
| 118 |
X = df.drop("hp_gain", axis=1)
|
|
@@ -126,128 +126,140 @@ def train_model():
|
|
| 126 |
|
| 127 |
return scaler, model
|
| 128 |
|
| 129 |
-
scaler, model =
|
| 130 |
-
|
| 131 |
-
# ---------------------------------------------------------
|
| 132 |
-
# 3. SIDEBAR CONTROLS (The "Settings Pane")
|
| 133 |
-
# ---------------------------------------------------------
|
| 134 |
|
|
|
|
|
|
|
|
|
|
| 135 |
with st.sidebar:
|
| 136 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
with st.expander("π§ Bolt-on Modifications", expanded=True):
|
| 148 |
-
intake = st.selectbox("Intake", ["Stock", "High Flow Filter", "Cold Air Intake"])
|
| 149 |
-
exhaust = st.selectbox("Exhaust", ["Stock", "Cat-back", "Long Tube Headers", "Full Straight Pipe"])
|
| 150 |
-
|
| 151 |
-
with st.expander("π₯ Internals & Boost", expanded=True):
|
| 152 |
induction = st.selectbox("Forced Induction", ["Naturally Aspirated", "Single Turbo", "Twin Turbo", "Supercharger"])
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
with st.expander("π» Tuning & Fuel", expanded=True):
|
| 157 |
-
fuel = st.selectbox("Fuel Type", ["87 Octane", "91 Octane", "93 Octane", "E85 (Ethanol)"])
|
| 158 |
-
tune = st.selectbox("ECU Map", ["Stock Map", "Stage 1", "Stage 2"])
|
| 159 |
-
|
| 160 |
-
# ---------------------------------------------------------
|
| 161 |
-
# 4. MAIN DASHBOARD (The "App View")
|
| 162 |
-
# ---------------------------------------------------------
|
| 163 |
-
|
| 164 |
-
# Prediction Mappings
|
| 165 |
-
intake_map = {"Stock":0, "High Flow Filter":1, "Cold Air Intake":2}
|
| 166 |
-
exhaust_map = {"Stock":0, "Cat-back":1, "Long Tube Headers":2, "Full Straight Pipe":3}
|
| 167 |
-
induction_map = {"Naturally Aspirated":0, "Single Turbo":1, "Twin Turbo":2, "Supercharger":3}
|
| 168 |
-
cams_map = {"Stock":0, "Street Profile":1, "Track/Race Profile":2}
|
| 169 |
-
nitrous_map = {False:0, True:1}
|
| 170 |
-
fuel_map = {"87 Octane":0, "91 Octane":1, "93 Octane":2, "E85 (Ethanol)":3}
|
| 171 |
-
tune_map = {"Stock Map":0, "Stage 1":1, "Stage 2":2}
|
| 172 |
-
|
| 173 |
-
# Calculation
|
| 174 |
-
input_data = np.array([[
|
| 175 |
-
engine, cyl, base_hp,
|
| 176 |
-
intake_map[intake], exhaust_map[exhaust], induction_map[induction],
|
| 177 |
-
cams_map[cams], nitrous_map[nitrous], fuel_map[fuel], tune_map[tune]
|
| 178 |
-
]])
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
<div class="dot red"></div>
|
| 190 |
-
<div class="dot yellow"></div>
|
| 191 |
-
<div class="dot green"></div>
|
| 192 |
-
</div>
|
| 193 |
-
""", unsafe_allow_html=True)
|
| 194 |
|
| 195 |
-
|
| 196 |
-
|
|
|
|
| 197 |
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
-
#
|
| 201 |
-
col1, col2 = st.columns([1
|
| 202 |
|
| 203 |
with col1:
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
"
|
| 208 |
-
"
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
st.vega_lite_chart(chart_data, {
|
| 213 |
-
"mark": {"type": "bar", "cornerRadiusEnd": 4, "color": "#007AFF"},
|
| 214 |
-
"encoding": {
|
| 215 |
-
"x": {"field": "Stage", "type": "nominal", "axis": {"labelAngle": 0}},
|
| 216 |
-
"y": {"field": "Horsepower", "type": "quantitative"},
|
| 217 |
-
"tooltip": ["Stage", "Horsepower"]
|
| 218 |
-
}
|
| 219 |
-
}, use_container_width=True)
|
| 220 |
|
| 221 |
with col2:
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
with st.container():
|
| 226 |
-
st.markdown(f"""
|
| 227 |
-
<div style="padding: 10px;">
|
| 228 |
-
<span style="font-size: 14px; color: #86868b; text-transform: uppercase; letter-spacing: 1px; font-weight: 600;">Total Power</span>
|
| 229 |
-
<div style="font-size: 48px; font-weight: 700; color: #1d1d1f; line-height: 1.2;">{int(new_hp)} HP</div>
|
| 230 |
-
</div>
|
| 231 |
-
""", unsafe_allow_html=True)
|
| 232 |
|
| 233 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
#
|
| 242 |
-
st.
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
|
|
|
| 4 |
from sklearn.preprocessing import StandardScaler
|
| 5 |
from sklearn.neighbors import KNeighborsRegressor
|
| 6 |
|
| 7 |
+
# ---------------------------
|
| 8 |
+
# 1. Configuration & Custom CSS (MacOS Theme)
|
| 9 |
+
# ---------------------------
|
| 10 |
+
st.set_page_config(
|
| 11 |
+
page_title="DynoPro for Mac",
|
| 12 |
+
page_icon="π₯οΈ",
|
| 13 |
+
layout="wide",
|
| 14 |
+
initial_sidebar_state="expanded"
|
| 15 |
+
)
|
| 16 |
|
| 17 |
+
# Custom CSS for the Apple/MacOS Aesthetic
|
| 18 |
st.markdown("""
|
| 19 |
+
<style>
|
| 20 |
+
/* Main Background - Apple Light Gray */
|
| 21 |
.stApp {
|
| 22 |
+
background-color: #F5F5F7;
|
| 23 |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 24 |
}
|
| 25 |
|
| 26 |
+
/* Sidebar - White with border */
|
| 27 |
section[data-testid="stSidebar"] {
|
| 28 |
+
background-color: #FFFFFF;
|
| 29 |
+
border-right: 1px solid #E5E5E5;
|
| 30 |
}
|
| 31 |
|
| 32 |
+
/* Headings */
|
| 33 |
+
h1, h2, h3 {
|
| 34 |
+
color: #1D1D1F;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
font-weight: 600;
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
+
/* Metrics - Card Style */
|
| 39 |
+
div[data-testid="stMetric"] {
|
| 40 |
+
background-color: #FFFFFF;
|
| 41 |
+
padding: 15px;
|
| 42 |
+
border-radius: 12px;
|
| 43 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
| 44 |
+
border: 1px solid #EAEAEA;
|
| 45 |
}
|
| 46 |
|
| 47 |
+
/* Custom Container Card */
|
| 48 |
+
.mac-card {
|
| 49 |
+
background-color: #FFFFFF;
|
| 50 |
+
padding: 20px;
|
| 51 |
+
border-radius: 16px;
|
| 52 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
| 53 |
margin-bottom: 20px;
|
| 54 |
}
|
| 55 |
+
|
| 56 |
+
/* Button Styling */
|
| 57 |
+
.stButton>button {
|
| 58 |
+
border-radius: 8px;
|
| 59 |
+
background-color: #0071E3;
|
| 60 |
+
color: white;
|
| 61 |
+
border: none;
|
| 62 |
+
}
|
| 63 |
+
</style>
|
| 64 |
+
""", unsafe_allow_html=True)
|
| 65 |
|
| 66 |
+
# ---------------------------
|
| 67 |
+
# 2. Advanced Dataset Generation
|
| 68 |
+
# ---------------------------
|
| 69 |
@st.cache_resource
|
| 70 |
+
def build_engine_model(n=800):
|
| 71 |
np.random.seed(42)
|
|
|
|
| 72 |
data = []
|
| 73 |
|
| 74 |
for _ in range(n):
|
| 75 |
# Expanded Engine Options
|
| 76 |
+
cyl = np.random.choice([3, 4, 5, 6, 8, 10, 12])
|
|
|
|
| 77 |
|
| 78 |
+
# Displacement logic based on cyl
|
| 79 |
+
if cyl <= 4: engine = np.random.uniform(1.0, 2.5)
|
| 80 |
+
elif cyl <= 6: engine = np.random.uniform(2.5, 4.0)
|
| 81 |
+
elif cyl <= 8: engine = np.random.uniform(4.0, 6.2)
|
| 82 |
+
else: engine = np.random.uniform(5.0, 8.4) # V10/V12
|
| 83 |
|
| 84 |
+
# Base HP Calculation (more realistic variance)
|
| 85 |
+
specific_output = np.random.uniform(60, 110) # HP per Liter
|
| 86 |
+
base_hp = int(engine * specific_output)
|
| 87 |
+
|
| 88 |
+
# Mods (0=Stock)
|
| 89 |
+
intake = np.random.choice([0, 1, 2]) # Stock/CAI/Race
|
| 90 |
+
headers = np.random.choice([0, 1]) # Stock/Aftermarket
|
| 91 |
+
exhaust = np.random.choice([0, 1, 2]) # Stock/Sport/Straight
|
| 92 |
induction = np.random.choice([0, 1, 2, 3]) # None/Turbo/Twin-Turbo/Super
|
| 93 |
+
intercooler = np.random.choice([0, 1]) # Stock/Upgraded
|
|
|
|
| 94 |
fuel = np.random.choice([0, 1, 2, 3]) # 87/91/93/E85
|
| 95 |
+
tune = np.random.choice([0, 1, 2, 3]) # None/Stage1/Stage2/Custom
|
| 96 |
|
| 97 |
+
# Logic: V12s gain more from exhaust/headers, Turbos gain huge from intercoolers
|
| 98 |
+
hp_gain = (
|
| 99 |
+
(intake * 4) +
|
| 100 |
+
(headers * (cyl * 1.5)) +
|
| 101 |
+
(exhaust * 8) +
|
| 102 |
+
(induction * (base_hp * 0.30)) + # % gain based on base HP
|
| 103 |
+
(intercooler * (15 if induction > 0 else 0)) +
|
| 104 |
+
(tune * 15) +
|
| 105 |
+
(fuel * 5) +
|
| 106 |
np.random.uniform(-5, 5)
|
| 107 |
)
|
| 108 |
|
| 109 |
+
# Diminishing returns for N/A engines
|
| 110 |
+
if induction == 0:
|
| 111 |
+
hp_gain = hp_gain * 0.7
|
| 112 |
|
| 113 |
+
data.append([engine, cyl, base_hp, intake, headers, exhaust, induction, intercooler, fuel, tune, hp_gain])
|
| 114 |
|
| 115 |
+
columns = ["engine", "cyl", "base_hp", "intake", "headers", "exhaust", "induction", "intercooler", "fuel", "tune", "hp_gain"]
|
| 116 |
df = pd.DataFrame(data, columns=columns)
|
| 117 |
|
| 118 |
X = df.drop("hp_gain", axis=1)
|
|
|
|
| 126 |
|
| 127 |
return scaler, model
|
| 128 |
|
| 129 |
+
scaler, model = build_engine_model()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
+
# ---------------------------
|
| 132 |
+
# 3. Sidebar (The "Control Center")
|
| 133 |
+
# ---------------------------
|
| 134 |
with st.sidebar:
|
| 135 |
+
st.title("ποΈ Tuner Studio")
|
| 136 |
+
st.caption("Vehicle Configuration")
|
| 137 |
+
|
| 138 |
+
st.subheader("Base Vehicle")
|
| 139 |
+
cyl = st.select_slider("Cylinders", options=[3, 4, 5, 6, 8, 10, 12], value=6)
|
| 140 |
+
engine = st.number_input("Displacement (L)", 1.0, 8.4, 3.0, step=0.1)
|
| 141 |
+
base_hp = st.number_input("Factory Horsepower", 80, 1200, 300, step=10)
|
| 142 |
+
|
| 143 |
+
st.markdown("---")
|
| 144 |
+
st.subheader("Modifications")
|
| 145 |
|
| 146 |
+
# Air & Exhaust
|
| 147 |
+
with st.expander("π¨ Air & Exhaust", expanded=True):
|
| 148 |
+
intake = st.selectbox("Intake", ["Stock", "Cold Air", "Race Intake"])
|
| 149 |
+
headers = st.toggle("Aftermarket Headers")
|
| 150 |
+
exhaust = st.select_slider("Exhaust System", options=["Stock", "Sport Cat-back", "Straight Pipe"])
|
| 151 |
+
|
| 152 |
+
# Engine & Power Adders
|
| 153 |
+
with st.expander("β‘ Induction & Tune", expanded=True):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
induction = st.selectbox("Forced Induction", ["Naturally Aspirated", "Single Turbo", "Twin Turbo", "Supercharger"])
|
| 155 |
+
intercooler = st.toggle("Upgraded Intercooler")
|
| 156 |
+
tune = st.select_slider("ECU Tune", options=["Stock Map", "Stage 1", "Stage 2", "Custom Dyno"])
|
| 157 |
+
fuel = st.selectbox("Fuel Type", ["87 Octane", "91 Octane", "93 Octane", "E85 / Race Gas"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
+
# ---------------------------
|
| 160 |
+
# 4. Processing
|
| 161 |
+
# ---------------------------
|
| 162 |
+
# Mappings
|
| 163 |
+
map_intake = {"Stock":0, "Cold Air":1, "Race Intake":2}
|
| 164 |
+
map_exhaust = {"Stock":0, "Sport Cat-back":1, "Straight Pipe":2}
|
| 165 |
+
map_induct = {"Naturally Aspirated":0, "Single Turbo":1, "Twin Turbo":2, "Supercharger":3}
|
| 166 |
+
map_tune = {"Stock Map":0, "Stage 1":1, "Stage 2":2, "Custom Dyno":3}
|
| 167 |
+
map_fuel = {"87 Octane":0, "91 Octane":1, "93 Octane":2, "E85 / Race Gas":3}
|
| 168 |
|
| 169 |
+
input_vector = np.array([[
|
| 170 |
+
engine, cyl, base_hp,
|
| 171 |
+
map_intake[intake],
|
| 172 |
+
1 if headers else 0,
|
| 173 |
+
map_exhaust[exhaust],
|
| 174 |
+
map_induct[induction],
|
| 175 |
+
1 if intercooler else 0,
|
| 176 |
+
map_fuel[fuel],
|
| 177 |
+
map_tune[tune]
|
| 178 |
+
]])
|
| 179 |
|
| 180 |
+
input_scaled = scaler.transform(input_vector)
|
| 181 |
+
pred_gain = model.predict(input_scaled)[0]
|
| 182 |
+
final_hp = base_hp + pred_gain
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
+
# ---------------------------
|
| 185 |
+
# 5. Main Dashboard (MacOS Style)
|
| 186 |
+
# ---------------------------
|
| 187 |
|
| 188 |
+
# Header
|
| 189 |
+
st.markdown("### ποΈ Dyno Simulation Results")
|
| 190 |
+
st.markdown("This dashboard estimates power output based on component synergy.")
|
| 191 |
+
st.write("") # Spacer
|
| 192 |
|
| 193 |
+
# Top Row: The "Hero" Cards
|
| 194 |
+
col1, col2 = st.columns([1, 2])
|
| 195 |
|
| 196 |
with col1:
|
| 197 |
+
# Summary Card
|
| 198 |
+
with st.container(border=True):
|
| 199 |
+
st.markdown("**Vehicle Profile**")
|
| 200 |
+
st.markdown(f"<h1 style='margin:0; font-size: 40px;'>{str(cyl)}<span style='font-size:20px; color:gray'>cyl</span></h1>", unsafe_allow_html=True)
|
| 201 |
+
st.caption(f"{engine}L Displacement")
|
| 202 |
+
st.divider()
|
| 203 |
+
st.write(f"**Induction:** {induction}")
|
| 204 |
+
st.write(f"**Tune:** {tune}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
with col2:
|
| 207 |
+
# Results Card
|
| 208 |
+
with st.container(border=True):
|
| 209 |
+
st.markdown("**Projected Output**")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
+
m1, m2, m3 = st.columns(3)
|
| 212 |
+
with m1:
|
| 213 |
+
st.metric("Base Power", f"{base_hp} HP")
|
| 214 |
+
with m2:
|
| 215 |
+
st.metric("Gain", f"+{pred_gain:.0f} HP", delta=f"{((pred_gain/base_hp)*100):.1f}%")
|
| 216 |
+
with m3:
|
| 217 |
+
st.metric("Total Power", f"{final_hp:.0f} HP", delta="Peak Output")
|
| 218 |
|
| 219 |
+
# Progress Bar visual
|
| 220 |
+
st.write("")
|
| 221 |
+
st.write("Power Utilization")
|
| 222 |
+
pct_gain = min((final_hp / (base_hp * 2)), 1.0) # Cap bar at 200% base
|
| 223 |
+
st.progress(pct_gain)
|
| 224 |
+
|
| 225 |
+
# Bottom Row: Visualization
|
| 226 |
+
st.write("")
|
| 227 |
+
st.markdown("### π Power Curve Analysis")
|
| 228 |
+
|
| 229 |
+
with st.container(border=True):
|
| 230 |
+
# Creating a synthetic RPM curve for visualization
|
| 231 |
+
rpms = np.linspace(2000, 8000, 50)
|
| 232 |
+
|
| 233 |
+
# Physics approximation for torque curve shape
|
| 234 |
+
def torque_curve(rpm, peak_hp):
|
| 235 |
+
peak_rpm = 6500
|
| 236 |
+
# simplified curve logic
|
| 237 |
+
return -((rpm - peak_rpm)**2) + (peak_hp * 5000)
|
| 238 |
+
|
| 239 |
+
# Normalized curves scaled to HP
|
| 240 |
+
base_curve = [base_hp * (1 - ((x - 6500)/5000)**2) * (x/8000) for x in rpms]
|
| 241 |
+
mod_curve = [final_hp * (1 - ((x - 6500)/5000)**2) * (x/8000) for x in rpms]
|
| 242 |
+
|
| 243 |
+
chart_data = pd.DataFrame({
|
| 244 |
+
"RPM": np.tile(rpms, 2),
|
| 245 |
+
"Horsepower": np.concatenate([base_curve, mod_curve]),
|
| 246 |
+
"Setup": ["Factory Stock"] * 50 + ["Modified"] * 50
|
| 247 |
+
})
|
| 248 |
|
| 249 |
+
# Use Streamlit's native area chart but configure it to look clean
|
| 250 |
+
st.area_chart(
|
| 251 |
+
chart_data,
|
| 252 |
+
x="RPM",
|
| 253 |
+
y="Horsepower",
|
| 254 |
+
color="Setup",
|
| 255 |
+
stack=False # Overlay them
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
# Footer / "Dock" feel
|
| 259 |
+
st.divider()
|
| 260 |
+
cols = st.columns(4)
|
| 261 |
+
cols[0].info(f"Fuel: {fuel}")
|
| 262 |
+
cols[1].info(f"Exhaust: {exhaust}")
|
| 263 |
+
cols[2].info(f"Headers: {'Yes' if headers else 'No'}")
|
| 264 |
+
cols[3].info(f"Intercooler: {'Yes' if intercooler else 'No'}")
|
| 265 |
|