NeuralGearheads's picture
Update src/streamlit_app.py
8555c27 verified
Raw
History Blame Contribute Delete
23.1 kB
import streamlit as st
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsRegressor
# ---------------------------
# Page config
# ---------------------------
st.set_page_config(
page_title="Neural Tuner – Car Mod Performance Estimator",
page_icon="🔥",
layout="wide"
)
# ---------------------------
# SESSION STATE INITIALIZATION (Fixes Yellow Warning)
# ---------------------------
def init_session_state():
# Only set these defaults if they don't exist yet
defaults = {
"engine_disp": 2.0,
"engine_layout": "I6",
"base_hp": 240,
"weight_kg": 1500,
"weight_reduction": 0,
"intake": "Stock",
"headers": "Stock",
"exhaust": "Stock",
"exhaust_dia": 60,
"cam": "Stock",
"intake_manifold": "Stock",
"induction": "None",
"boost_psi": 0,
"turbo_size": "N/A",
"intercooler": "None",
"meth": False,
"tune": "None",
"fuel": "93",
"altitude": 200,
"traction_mode": "Spirited"
}
for key, value in defaults.items():
if key not in st.session_state:
st.session_state[key] = value
# Run init immediately
init_session_state()
# ---------------------------
# Global CSS (Fixes "Cutting Out" / Rounds)
# ---------------------------
st.markdown(
"""
<style>
html, body, [data-testid="stAppViewContainer"] {
background: radial-gradient(circle at top, #020617 0, #020617 35%, #020617 40%, #000000 100%) !important;
color: #e5e7eb;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text",
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
[data-testid="stHeader"] { background: transparent; }
[data-testid="stToolbar"] { display: none; }
/* Fix: Removed overflow:hidden to stop cutting off shadows */
.hero {
border-radius: 24px;
padding: 24px 28px;
background: radial-gradient(circle at top left, rgba(96,165,250,0.35), transparent 55%),
radial-gradient(circle at bottom right, rgba(236,72,153,0.35), transparent 55%),
rgba(15,23,42,0.94);
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
border: 1px solid rgba(148,163,184,0.35);
position: relative;
margin-bottom: 20px;
}
.hero-title {
font-size: 1.9rem;
font-weight: 700;
background: linear-gradient(90deg, #f97316, #facc15, #22c55e, #38bdf8, #a855f7, #f97316);
background-size: 400% 100%;
-webkit-background-clip: text;
color: transparent;
animation: moveGradient 9s ease infinite;
}
.hero-sub {
color: #9ca3af;
font-size: 0.95rem;
}
.hero-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 11px;
border-radius: 999px;
background: rgba(15,118,110,0.2);
border: 1px solid rgba(34,197,94,0.6);
font-size: 0.75rem;
color: #bbf7d0;
margin-right: 8px;
}
@keyframes moveGradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Fix: Removed overflow:hidden and added margin for shadow breathing room */
.glass {
background: radial-gradient(circle at top left, rgba(148,163,184,0.24), transparent 55%),
rgba(15,23,42,0.96);
border-radius: 20px;
padding: 20px;
border: 1px solid rgba(148,163,184,0.4);
box-shadow: 0 15px 35px rgba(0,0,0,0.6);
margin: 5px;
}
.chip {
display:inline-block;
padding:4px 10px;
margin:3px 4px 3px 0;
border-radius:999px;
font-size:0.78rem;
background:rgba(59,130,246,0.14);
border:1px solid rgba(59,130,246,0.45);
color:#bfdbfe;
}
.meter-label {
font-size: 0.85rem;
color: #9ca3af;
margin-bottom: 3px;
}
.muted {
font-size: 0.8rem;
color: #6b7280;
}
/* Tabs styling */
button[data-baseweb="tab"] {
background: transparent !important;
border-radius: 999px !important;
padding: 0.5rem 1rem !important;
margin-right: 0.4rem;
color: #9ca3af !important;
}
button[data-baseweb="tab"][aria-selected="true"] {
background: rgba(59,130,246,0.2) !important;
color: #e5e7eb !important;
box-shadow: 0 0 0 1px rgba(59,130,246,0.7);
}
/* Rev limiter gauge - Only this keeps overflow hidden for the semi-circle */
.rev-wrap {
display:flex;
justify-content:center;
align-items:center;
margin-top:4px;
margin-bottom:4px;
}
.rev-gauge {
position:relative;
width:170px;
height:90px;
border-radius:170px 170px 14px 14px;
background: radial-gradient(circle at 50% 120%, #991b1b, transparent 60%),
radial-gradient(circle at 0% 0%, rgba(59,130,246,0.4), transparent 60%),
#020617;
overflow:hidden;
border:1px solid rgba(148,163,184,0.5);
box-shadow: 0 12px 30px rgba(0,0,0,0.8);
}
.rev-arc {
position:absolute;
inset:10px 10px auto 10px;
height:62px;
border-radius:999px;
border-top:6px solid rgba(249,250,251,0.16);
border-left:6px solid rgba(34,197,94,0.75);
border-right:6px solid rgba(220,38,38,0.9);
border-bottom:none;
}
.rev-needle {
--base-angle: -40deg;
position:absolute;
left:50%;
bottom:8px;
width:3px;
height:68px;
background: linear-gradient(to top, #fecaca, #f97316);
border-radius:999px;
transform-origin:50% 90%;
animation: revBounce 0.6s ease-in-out infinite alternate;
}
.rev-center {
position:absolute;
left:50%;
bottom:4px;
transform:translateX(-50%);
width:22px;
height:22px;
border-radius:999px;
background:#020617;
border:2px solid #9ca3af;
display:flex;
align-items:center;
justify-content:center;
font-size:0.55rem;
color:#9ca3af;
}
.rev-label {
position:absolute;
right:8px;
bottom:6px;
font-size:0.7rem;
color:#fecaca;
}
@keyframes revBounce {
from { transform: rotate(var(--base-angle)); }
to { transform: rotate(calc(var(--base-angle) + 14deg)); }
}
</style>
""",
unsafe_allow_html=True
)
# ---------------------------
# Data & model
# ---------------------------
def generate_dataset(n=400):
np.random.seed(42)
data = []
for _ in range(n):
engine = np.random.choice([1.6, 2.0, 2.5, 3.0, 3.5, 5.0])
cyl = np.random.choice([4, 6, 8])
base_hp = int(engine * cyl * np.random.uniform(18, 22))
intake = np.random.choice([0, 1, 2])
exhaust = np.random.choice([0, 1, 2])
induction = np.random.choice([0, 1, 2])
fuel = np.random.choice([0, 1, 2, 3])
tune = np.random.choice([0, 1, 2])
altitude = np.random.uniform(0, 2000)
hp_gain = (
intake * np.random.uniform(3, 10) +
exhaust * np.random.uniform(5, 20) +
induction * np.random.uniform(25, 100) +
tune * np.random.uniform(10, 35) +
fuel * np.random.uniform(2, 8) -
altitude * 0.01 +
np.random.uniform(-3, 3)
)
data.append([engine, cyl, base_hp, intake, exhaust,
induction, fuel, tune, altitude, hp_gain])
columns = [
"engine", "cyl", "base_hp", "intake", "exhaust",
"induction", "fuel", "tune", "altitude", "hp_gain"
]
return pd.DataFrame(data, columns=columns)
df = generate_dataset()
X = df.drop("hp_gain", axis=1)
y = df["hp_gain"]
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
model = KNeighborsRegressor(n_neighbors=5, weights="distance")
model.fit(X_scaled, y)
# ---------------------------
# PRESET CALLBACKS
# ---------------------------
def preset_daily():
s = st.session_state
s.engine_disp = 1.6
s.engine_layout = "I4"
s.base_hp = 120
s.weight_kg = 1350
s.weight_reduction = 0
s.intake = "Stock"
s.headers = "Stock"
s.exhaust = "Stock"
s.exhaust_dia = 50
s.cam = "Stock"
s.intake_manifold = "Stock"
s.induction = "None"
s.boost_psi = 0
s.turbo_size = "N/A"
s.intercooler = "None"
s.meth = False
s.tune = "None"
s.fuel = "87"
s.altitude = 200
s.traction_mode = "Daily"
def preset_street():
s = st.session_state
s.engine_disp = 2.0
s.engine_layout = "I4"
s.base_hp = 190
s.weight_kg = 1400
s.weight_reduction = 5
s.intake = "Cold Air"
s.headers = "Shorty"
s.exhaust = "Cat-back"
s.exhaust_dia = 60
s.cam = "Stage 1 – Road"
s.intake_manifold = "High-flow"
s.induction = "Turbo"
s.boost_psi = 12
s.turbo_size = "Small 45-55mm"
s.intercooler = "Air-to-Air"
s.meth = False
s.tune = "Stage 1"
s.fuel = "93"
s.altitude = 150
s.traction_mode = "Spirited"
def preset_drag():
s = st.session_state
s.engine_disp = 5.0
s.engine_layout = "V8"
s.base_hp = 420
s.weight_kg = 1500
s.weight_reduction = 18
s.intake = "Performance"
s.headers = "Long Tube"
s.exhaust = "Straight Pipe"
s.exhaust_dia = 90
s.cam = "Stage 2 – Aggressive"
s.intake_manifold = "High-flow"
s.induction = "Twin-Turbo"
s.boost_psi = 24
s.turbo_size = "Big 66-75mm"
s.intercooler = "Front-mount High-Flow"
s.meth = True
s.tune = "Kill Mode"
s.fuel = "E85 / Race Blend"
s.altitude = 100
s.traction_mode = "Track / Drag"
def preset_max():
s = st.session_state
s.engine_disp = 6.0
s.engine_layout = "V12"
s.base_hp = 650
s.weight_kg = 1400
s.weight_reduction = 25
s.intake = "Performance"
s.headers = "Long Tube"
s.exhaust = "Straight Pipe"
s.exhaust_dia = 100
s.cam = "Stage 3 – Race"
s.intake_manifold = "Individual throttle bodies"
s.induction = "Twincharged"
s.boost_psi = 30
s.turbo_size = "XL 76mm+"
s.intercooler = "Front-mount High-Flow"
s.meth = True
s.tune = "Kill Mode"
s.fuel = "E85 / Race Blend"
s.altitude = 0
s.traction_mode = "Track / Drag"
# ---------------------------
# HERO SECTION
# ---------------------------
st.markdown(
"""
<div class="hero">
<div style="display:flex;justify-content:space-between;align-items:center;gap:14px;">
<div>
<div class="hero-pill">⚙️ KNN model + synthetic dyno data</div>
<div class="hero-pill">🎛️ Live tuning playground</div>
<div class="hero-title">Neural Tuner – Car Mod Performance Lab</div>
<p class="hero-sub">
Mash intakes, boost, tunes and fuel in real time.
Watch horsepower climb and the rev limiter dance. Built to make car nerds go feral. 🐺
</p>
</div>
<div style="
width:170px;height:110px;
border-radius:22px;
background:conic-gradient(from 220deg,
#22c55e, #38bdf8, #a855f7, #f97316, #facc15, #22c55e);
padding:2px;
">
<div style="
width:100%;height:100%;
border-radius:19px;
background:radial-gradient(circle at 30% 0%, rgba(248,250,252,0.2), transparent 55%),
#020617;">
<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;">
<div style="font-size:0.78rem;color:#9ca3af;">Virtual</div>
<div style="font-size:1.6rem;font-weight:700;color:#e5e7eb;">Dyno</div>
<div style="font-size:0.75rem;color:#22c55e;">No hardware, all chaos</div>
</div>
</div>
</div>
</div>
</div>
""",
unsafe_allow_html=True
)
st.markdown("")
# ---------------------------
# Layout
# ---------------------------
left, right = st.columns([1.15, 1])
# ========= LEFT SIDE =========
with left:
st.markdown('<div class="glass">', unsafe_allow_html=True)
st.subheader("🎛️ Tune Your Build")
# Preset buttons
bcol1, bcol2, bcol3, bcol4 = st.columns(4)
bcol1.button("🚙 Daily Driver", on_click=preset_daily)
bcol2.button("🌃 Street Fun", on_click=preset_street)
bcol3.button("🏁 Drag Strip", on_click=preset_drag)
bcol4.button("🔥 Max Attack", on_click=preset_max)
st.markdown("")
tabs = st.tabs(["Core Specs", "Bolt-Ons", "Boost & Cooling", "ECU & Fuel"])
# NOTE: I removed 'index=...' from these selectboxes.
# They now just rely on the key being present in st.session_state (handled by init_session_state)
# --- Core specs ---
with tabs[0]:
col1, col2 = st.columns(2)
engine_disp_options = [0.8,1.0,1.2,1.4,1.6,1.8,2.0,2.2,2.4,2.5,
2.8,3.0,3.2,3.5,4.0,4.4,5.0,6.0,7.0,8.0]
# We manually update value if it's missing (failsafe), though init handles it.
if "engine_disp" not in st.session_state: st.session_state.engine_disp = 2.0
engine_disp = col1.selectbox(
"Engine Displacement (L)",
engine_disp_options,
key="engine_disp"
)
engine_layout = col2.selectbox(
"Engine Layout",
["I3","I4","I6","V6","V8","V10","V12"],
key="engine_layout"
)
layout_to_cyl = {"I3":3,"I4":4,"I6":6,"V6":6,"V8":8,"V10":10,"V12":12}
cyl = layout_to_cyl[engine_layout]
base_hp = st.number_input(
"Base Horsepower (stock dyno)",
min_value=60, max_value=3000,
key="base_hp"
)
col3, col4 = st.columns(2)
weight_kg = col3.number_input(
"Vehicle Weight (kg)",
min_value=700, max_value=4000,
key="weight_kg"
)
weight_reduction = col4.slider(
"Weight Reduction (%)",
0, 40,
key="weight_reduction"
)
# --- Bolt-ons ---
with tabs[1]:
col1, col2, col3 = st.columns(3)
intake = col1.selectbox("Intake", ["Stock", "Cold Air", "Performance"], key="intake")
headers = col2.selectbox("Headers", ["Stock", "Shorty", "Long Tube"], key="headers")
exhaust = col3.selectbox("Exhaust", ["Stock", "Cat-back", "Straight Pipe"], key="exhaust")
exhaust_dia = st.slider("Exhaust Diameter (mm)", 40, 120, key="exhaust_dia")
cam = st.selectbox(
"Cam Profile",
["Stock", "Stage 1 – Road", "Stage 2 – Aggressive", "Stage 3 – Race"],
key="cam"
)
intake_manifold = st.selectbox(
"Intake Manifold",
["Stock", "High-flow", "Individual throttle bodies"],
key="intake_manifold"
)
# --- Boost & cooling ---
with tabs[2]:
induction = st.selectbox(
"Forced Induction Setup",
["None", "Turbo", "Twin-Turbo", "Supercharger", "Twincharged"],
key="induction"
)
boost_psi = st.slider("Target Boost (psi)", 0, 40, key="boost_psi")
turbo_size = st.selectbox(
"Turbo Size",
["N/A", "Small 45-55mm", "Medium 56-65mm", "Big 66-75mm", "XL 76mm+"],
key="turbo_size"
)
intercooler = st.selectbox(
"Intercooler Type",
["None", "Air-to-Air", "Air-to-Water", "Front-mount High-Flow"],
key="intercooler"
)
meth = st.checkbox("Methanol Injection Kit", key="meth")
# --- ECU & fuel ---
with tabs[3]:
tune = st.selectbox(
"ECU Tune Level",
["None", "Mild Street", "Stage 1", "Stage 2", "Kill Mode"],
key="tune"
)
fuel = st.selectbox(
"Fuel Type",
["87", "91", "93", "E85 / Race Blend"],
key="fuel"
)
altitude = st.slider("Altitude (meters)", 0, 3500, key="altitude")
traction_mode = st.radio(
"Traction Mode Vibe",
["Daily", "Spirited", "Track / Drag"],
horizontal=True,
key="traction_mode"
)
st.markdown(
'<p class="muted" style="margin-top:8px;">Numbers are synthetic; this is a dyno-inspired playground, not a tuning bible. 🧪</p>',
unsafe_allow_html=True
)
st.markdown('</div>', unsafe_allow_html=True)
# ========= MODEL & CALCS =========
intake_map = {"Stock":0, "Cold Air":1, "Performance":2}
exhaust_map = {"Stock":0, "Cat-back":1, "Straight Pipe":2}
induction_model_map = {"None":0, "Turbo":1, "Twin-Turbo":1, "Supercharger":2, "Twincharged":2}
fuel_map = {"87":0, "91":1, "93":2, "E85 / Race Blend":3}
tune_base_map = {"None":0, "Mild Street":1, "Stage 1":1, "Stage 2":2, "Kill Mode":2}
input_for_model = np.array([[
engine_disp,
cyl,
base_hp,
intake_map.get(intake,0),
exhaust_map.get(exhaust,0),
induction_model_map.get(induction,0),
fuel_map.get(fuel,0),
tune_base_map.get(tune,0),
altitude
]])
input_scaled = scaler.transform(input_for_model)
pred_base = float(model.predict(input_scaled)[0])
cam_gain_map = {
"Stock":0.0,
"Stage 1 – Road":6.0,
"Stage 2 – Aggressive":12.0,
"Stage 3 – Race":20.0
}
headers_gain_map = {"Stock":0.0, "Shorty":5.0, "Long Tube":9.0}
intake_man_gain_map = {
"Stock":0.0,
"High-flow":5.0,
"Individual throttle bodies":10.0
}
intercooler_gain_map = {
"None":0.0,
"Air-to-Air":4.0,
"Air-to-Water":6.5,
"Front-mount High-Flow":9.0
}
turbo_size_map = {
"N/A":0.0,
"Small 45-55mm":5.0,
"Medium 56-65mm":12.0,
"Big 66-75mm":20.0,
"XL 76mm+":28.0
}
cam_gain = cam_gain_map.get(cam, 0.0)
headers_gain = headers_gain_map.get(headers, 0.0)
intake_man_gain = intake_man_gain_map.get(intake_manifold, 0.0)
intercooler_gain = intercooler_gain_map.get(intercooler, 0.0)
turbo_size_gain = turbo_size_map.get(turbo_size, 0.0)
if induction in ["Turbo", "Twin-Turbo"]:
boost_gain = boost_psi * 1.8
elif induction in ["Supercharger", "Twincharged"]:
boost_gain = boost_psi * 1.4
else:
boost_gain = 0.0
meth_gain = 12.0 if meth else 0.0
exhaust_dia_gain = max(0.0, (exhaust_dia - 55) * 0.1)
extra_gain = (
cam_gain +
headers_gain +
intake_man_gain +
intercooler_gain +
turbo_size_gain +
boost_gain * 0.9 +
meth_gain +
exhaust_dia_gain
)
traction_multiplier = {"Daily":0.9, "Spirited":1.0, "Track / Drag":1.05}[traction_mode]
pred_total_gain = (pred_base + extra_gain) * traction_multiplier
pred_total_gain = max(pred_total_gain, -5.0)
new_hp = max(base_hp + pred_total_gain, 40.0)
effective_weight = weight_kg * (1 - weight_reduction / 100.0)
hp_per_ton = new_hp / (effective_weight / 1000.0)
hype_raw = np.clip(pred_total_gain / 120 * 100, 0, 100)
hype_level = int(hype_raw)
if hype_level < 20:
verdict = "Sleeper grocery getter 🚙"
elif hype_level < 40:
verdict = "Respectable street build 🌃"
elif hype_level < 70:
verdict = "Serious weekend weapon ⚔️"
else:
verdict = "Full send, tyres cry for mercy 🏁"
# compute base angle for rev gauge (-90deg to +90deg)
angle_deg = -90 + (hype_level / 100.0) * 180
# ========= RIGHT SIDE =========
with right:
st.markdown('<div class="glass">', unsafe_allow_html=True)
st.subheader("💥 Build Outcome")
m1, m2, m3 = st.columns(3)
m1.metric("Estimated HP Gain", f"{pred_total_gain:.1f} HP")
m2.metric("New Output", f"{new_hp:.1f} HP")
m3.metric("HP per Ton", f"{hp_per_ton:.1f}")
# Animated rev limiter gauge
st.markdown('<div class="meter-label">Rev Limiter Vibes</div>', unsafe_allow_html=True)
st.markdown(
f"""
<div class="rev-wrap">
<div class="rev-gauge">
<div class="rev-arc"></div>
<div class="rev-needle" style="--base-angle:{angle_deg}deg;"></div>
<div class="rev-center">RPM</div>
<div class="rev-label">REDLINE</div>
</div>
</div>
""",
unsafe_allow_html=True
)
st.markdown('<div class="meter-label">Hype Meter (relative craziness of this build)</div>', unsafe_allow_html=True)
st.progress(hype_level)
st.markdown(f"**Verdict:** {verdict}")
st.markdown("")
st.bar_chart(
pd.DataFrame(
{"Horsepower": [base_hp, new_hp]},
index=["Stock", "Tuned"]
)
)
st.markdown(
f"""
<div style="margin-top:6px;">
<span class="chip">{engine_disp}L {engine_layout}</span>
<span class="chip">Weight: {weight_kg} kg ▸ −{weight_reduction}%</span>
<span class="chip">Intake: {intake}</span>
<span class="chip">Headers: {headers}</span>
<span class="chip">Exhaust: {exhaust} ({exhaust_dia}mm)</span><br/>
<span class="chip">Induction: {induction} @ {boost_psi} psi</span>
<span class="chip">IC: {intercooler}</span>
<span class="chip">Tune: {tune}</span>
<span class="chip">Fuel: {fuel}</span>
</div>
""",
unsafe_allow_html=True
)
st.markdown("---")
breakdown = pd.DataFrame({
"Component": [
"Model base (from dataset)",
"Cam profile",
"Headers",
"Intake manifold",
"Intercooler",
"Turbo size",
"Boost (net)",
"Meth kit",
"Exhaust diameter tweak"
],
"Approx HP": [
pred_base,
cam_gain,
headers_gain,
intake_man_gain,
intercooler_gain,
turbo_size_gain,
boost_gain * 0.9,
meth_gain,
exhaust_dia_gain
]
})
st.caption("🔬 Where is that extra power coming from?")
st.dataframe(breakdown, use_container_width=True, height=260)
st.markdown(
'<p class="muted" style="margin-top:8px;">Model: distance-weighted KNN on synthetic dyno-style data + heuristic math for advanced mods.</p>',
unsafe_allow_html=True
)
st.markdown('</div>', unsafe_allow_html=True)
# ========= FOOTER =========
st.markdown(
"""
<div style="text-align:center;margin-top:10px;" class="muted">
Presets + rev limiter + neon dyno… if this doesn’t make people go mad, we can push it even further. 🔧🔥
</div>
""",
unsafe_allow_html=True
)