NeuralGearheads commited on
Commit
945ffe8
Β·
verified Β·
1 Parent(s): a22e7a9

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +440 -218
src/streamlit_app.py CHANGED
@@ -4,247 +4,469 @@ import numpy as np
4
  from sklearn.preprocessing import StandardScaler
5
  from sklearn.neighbors import KNeighborsRegressor
6
 
7
- # ---------------------------------------------------------
8
- # 1. macOS STYLING (FIXED FOR DARK MODE ISSUES)
9
- # ---------------------------------------------------------
10
- st.set_page_config(page_title="Mac-Mod Tuner", page_icon="πŸ–₯️", layout="wide")
11
-
12
- st.markdown("""
13
- <style>
14
- /* FORCE TEXT COLOR TO BLACK (Overrides System Dark Mode) */
15
- .stApp, .stMarkdown, p, h1, h2, h3, h4, h5, h6, span, div, label {
16
- color: #1d1d1f !important;
 
 
 
 
 
 
 
 
 
 
17
  }
18
-
19
- /* Main Background - Apple Light Grey */
20
- .stApp {
21
- background-color: #f5f5f7;
22
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
 
 
 
 
 
 
 
 
 
 
23
  }
24
-
25
- /* Sidebar Styling */
26
- section[data-testid="stSidebar"] {
27
- background-color: #e8e8ed;
28
- border-right: 1px solid #d1d1d6;
 
 
 
 
29
  }
30
-
31
- /* Fix Sidebar Text Colors specifically */
32
- section[data-testid="stSidebar"] * {
33
- color: #1d1d1f !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
-
36
- /* Card/Container Styling */
37
- div[data-testid="stVerticalBlock"] > div[style*="background-color"] {
38
- background-color: white;
39
  border-radius: 18px;
40
- padding: 20px;
41
- box-shadow: 0 4px 20px rgba(0,0,0,0.05);
42
- border: 1px solid #e5e5ea;
43
  }
44
 
45
- /* Input Widgets - Fix text inside inputs */
46
- .stSelectbox > div > div, .stNumberInput > div > div > input {
47
- background-color: white !important;
48
- color: #1d1d1f !important;
49
- border-radius: 12px;
50
- border: 1px solid #d1d1d6;
 
 
 
51
  }
52
-
53
- /* Dropdown menu text fix */
54
- ul[data-testid="stSelectboxVirtualDropdown"] li {
55
- color: black !important;
 
56
  }
57
-
58
- /* Metrics Styling */
59
- div[data-testid="stMetricValue"] {
60
- font-weight: 700 !important;
61
- color: #1d1d1f !important;
62
  }
63
- div[data-testid="stMetricLabel"] {
64
- color: #86868b !important;
 
 
 
 
 
 
65
  }
66
-
67
- /* Custom "Traffic Lights" */
68
- .traffic-lights {
69
- display: flex;
70
- gap: 8px;
71
- margin-bottom: 20px;
72
  }
73
- .dot { width: 12px; height: 12px; border-radius: 50%; }
74
- .red { background-color: #ff5f57; border: 1px solid #e0443e; }
75
- .yellow { background-color: #febc2e; border: 1px solid #d89e24; }
76
- .green { background-color: #28c840; border: 1px solid #1aab29; }
77
- </style>
78
- """, unsafe_allow_html=True)
79
-
80
- # ---------------------------------------------------------
81
- # 2. DATA & MODEL (V12 & NITROUS)
82
- # ---------------------------------------------------------
83
-
84
- @st.cache_resource
85
- def train_model():
86
  np.random.seed(42)
87
- n = 1000
88
  data = []
89
-
90
  for _ in range(n):
91
- engine = np.random.choice([1.6, 2.0, 2.4, 3.0, 3.8, 4.0, 5.0, 5.2, 6.0, 6.5])
92
- cyl = np.random.choice([4, 6, 8, 10, 12])
93
- base_hp = int(engine * np.random.uniform(60, 110))
94
-
95
  intake = np.random.choice([0, 1, 2])
96
- exhaust = np.random.choice([0, 1, 2, 3])
97
- induction = np.random.choice([0, 1, 2, 3])
98
- cams = np.random.choice([0, 1, 2])
99
- nitrous = np.random.choice([0, 1])
100
  fuel = np.random.choice([0, 1, 2, 3])
101
  tune = np.random.choice([0, 1, 2])
102
-
103
- gain = (
104
- (intake * 3) +
105
- (exhaust * 5) +
106
- (induction * (base_hp * 0.35)) +
107
- (cams * (base_hp * 0.10)) +
108
- (nitrous * 50) +
109
- (tune * (base_hp * 0.08)) +
110
- (fuel * 4) +
111
- np.random.uniform(-5, 5)
112
  )
113
-
114
- if cyl < 6 and gain > 200:
115
- gain *= 0.8
116
-
117
- data.append([engine, cyl, base_hp, intake, exhaust, induction, cams, nitrous, fuel, tune, gain])
118
-
119
- columns = ["engine", "cyl", "base_hp", "intake", "exhaust", "induction", "cams", "nitrous", "fuel", "tune", "hp_gain"]
120
- df = pd.DataFrame(data, columns=columns)
121
-
122
- X = df.drop("hp_gain", axis=1)
123
- y = df["hp_gain"]
124
-
125
- scaler = StandardScaler()
126
- X_scaled = scaler.fit_transform(X)
127
-
128
- model = KNeighborsRegressor(n_neighbors=7, weights='distance')
129
- model.fit(X_scaled, y)
130
-
131
- return scaler, model
132
-
133
- scaler, model = train_model()
134
-
135
- # ---------------------------------------------------------
136
- # 3. SIDEBAR CONTROLS
137
- # ---------------------------------------------------------
138
-
139
- with st.sidebar:
140
- st.header("βš™οΈ Configuration")
141
- st.markdown("---")
142
-
143
- with st.expander("πŸš™ Base Vehicle Stats", expanded=True):
144
- col_eng_1, col_eng_2 = st.columns(2)
145
- with col_eng_1:
146
- cyl = st.selectbox("Cylinders", [4, 5, 6, 8, 10, 12])
147
- with col_eng_2:
148
- engine = st.selectbox("Size (L)", [1.6, 2.0, 2.5, 3.0, 3.5, 4.0, 5.0, 5.2, 6.0, 6.5, 8.4])
149
-
150
- base_hp = st.number_input("Factory HP", 100, 1000, 300)
151
-
152
- with st.expander("πŸ”§ Bolt-on Modifications", expanded=True):
153
- intake = st.selectbox("Intake", ["Stock", "High Flow Filter", "Cold Air Intake"])
154
- exhaust = st.selectbox("Exhaust", ["Stock", "Cat-back", "Long Tube Headers", "Full Straight Pipe"])
155
-
156
- with st.expander("πŸ”₯ Internals & Boost", expanded=True):
157
- induction = st.selectbox("Forced Induction", ["Naturally Aspirated", "Single Turbo", "Twin Turbo", "Supercharger"])
158
- cams = st.selectbox("Camshafts", ["Stock", "Street Profile", "Track/Race Profile"])
159
- nitrous = st.checkbox("Nitrous Oxide System (NOS)", value=False)
160
-
161
- with st.expander("πŸ’» Tuning & Fuel", expanded=True):
162
- fuel = st.selectbox("Fuel Type", ["87 Octane", "91 Octane", "93 Octane", "E85 (Ethanol)"])
163
- tune = st.selectbox("ECU Map", ["Stock Map", "Stage 1", "Stage 2"])
164
-
165
- # ---------------------------------------------------------
166
- # 4. MAIN DASHBOARD
167
- # ---------------------------------------------------------
168
-
169
- # Mappings
170
- intake_map = {"Stock":0, "High Flow Filter":1, "Cold Air Intake":2}
171
- exhaust_map = {"Stock":0, "Cat-back":1, "Long Tube Headers":2, "Full Straight Pipe":3}
172
- induction_map = {"Naturally Aspirated":0, "Single Turbo":1, "Twin Turbo":2, "Supercharger":3}
173
- cams_map = {"Stock":0, "Street Profile":1, "Track/Race Profile":2}
174
- nitrous_map = {False:0, True:1}
175
- fuel_map = {"87 Octane":0, "91 Octane":1, "93 Octane":2, "E85 (Ethanol)":3}
176
- tune_map = {"Stock Map":0, "Stage 1":1, "Stage 2":2}
177
-
178
- # Logic
179
- input_data = np.array([[
180
- engine, cyl, base_hp,
181
- intake_map[intake], exhaust_map[exhaust], induction_map[induction],
182
- cams_map[cams], nitrous_map[nitrous], fuel_map[fuel], tune_map[tune]
183
- ]])
184
 
185
- pred = model.predict(scaler.transform(input_data))[0]
186
- new_hp = base_hp + pred
187
- pct_gain = (pred / base_hp) * 100
 
 
 
 
 
 
 
 
 
188
 
189
- # UI Header
190
- st.markdown("""
191
- <div class="traffic-lights">
192
- <div class="dot red"></div>
193
- <div class="dot yellow"></div>
194
- <div class="dot green"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  </div>
196
- """, unsafe_allow_html=True)
 
 
197
 
198
- st.title("Performance Estimator Pro")
199
- st.markdown(f"Analysis for **{cyl}-Cylinder {engine}L Engine**")
200
- st.divider()
201
 
202
- # UI Body
203
- col1, col2 = st.columns([1.5, 1])
 
 
204
 
205
- with col1:
206
- st.markdown("### πŸ“ˆ Dyno Projection")
207
- chart_data = pd.DataFrame({
208
- "Horsepower": [base_hp, new_hp],
209
- "Stage": ["Factory", "Modified"]
210
- })
211
-
212
- st.vega_lite_chart(chart_data, {
213
- "mark": {"type": "bar", "cornerRadiusEnd": 6, "color": "#007AFF"},
214
- "encoding": {
215
- "x": {"field": "Stage", "type": "nominal", "axis": {"labelAngle": 0, "labelColor": "#1d1d1f"}},
216
- "y": {"field": "Horsepower", "type": "quantitative", "axis": {"labelColor": "#1d1d1f"}},
217
- "tooltip": ["Stage", "Horsepower"]
218
- }
219
- }, use_container_width=True)
220
-
221
- with col2:
222
- st.markdown("### ⚑ Results")
223
-
224
- with st.container():
225
- # Custom HTML Metric for better styling
226
- st.markdown(f"""
227
- <div style="background-color: white; border-radius: 12px; padding: 15px; border: 1px solid #e5e5ea;">
228
- <div style="font-size: 14px; color: #86868b; text-transform: uppercase; font-weight: 600;">Total Power Output</div>
229
- <div style="font-size: 42px; font-weight: 800; color: #1d1d1f; letter-spacing: -1px;">{int(new_hp)} HP</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  </div>
231
- """, unsafe_allow_html=True)
232
-
233
- st.markdown("<br>", unsafe_allow_html=True)
234
-
235
- c1, c2 = st.columns(2)
236
- with c1:
237
- st.metric("Gain", f"+{int(pred)} HP")
238
- with c2:
239
- st.metric("Improvement", f"{pct_gain:.1f}%")
240
-
241
- st.markdown("### πŸ“‹ Build Summary")
242
- with st.container():
243
- c1, c2, c3, c4 = st.columns(4)
244
- c1.info(f"**Induction:** {induction}")
245
- c2.info(f"**Fuel:** {fuel}")
246
- c3.info(f"**Camshafts:** {cams}")
247
- c4.info(f"**Nitrous:** {'Enabled' if nitrous else 'Disabled'}")
248
-
249
- if pct_gain > 50:
250
- st.toast("πŸš€ Massive gains! Ensure engine internals are forged.", icon="⚠️")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  from sklearn.preprocessing import StandardScaler
5
  from sklearn.neighbors import KNeighborsRegressor
6
 
7
+ # ---------------------------
8
+ # Page config
9
+ # ---------------------------
10
+ st.set_page_config(
11
+ page_title="Neural Tuner – Car Mod Performance Estimator",
12
+ page_icon="πŸ”₯",
13
+ layout="wide"
14
+ )
15
+
16
+ # ---------------------------
17
+ # Global CSS – crazy but clean
18
+ # ---------------------------
19
+ st.markdown(
20
+ """
21
+ <style>
22
+ html, body, [data-testid="stAppViewContainer"] {
23
+ background: radial-gradient(circle at top, #020617 0, #020617 35%, #020617 40%, #000000 100%) !important;
24
+ color: #e5e7eb;
25
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text",
26
+ "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
27
  }
28
+
29
+ /* Hide default Streamlit header/menu */
30
+ [data-testid="stHeader"] { background: transparent; }
31
+ [data-testid="stToolbar"] { display: none; }
32
+
33
+ .hero {
34
+ border-radius: 24px;
35
+ padding: 18px 22px;
36
+ background: radial-gradient(circle at top left, rgba(96,165,250,0.35), transparent 55%),
37
+ radial-gradient(circle at bottom right, rgba(236,72,153,0.35), transparent 55%),
38
+ rgba(15,23,42,0.94);
39
+ box-shadow: 0 25px 60px rgba(15,23,42,0.9);
40
+ border: 1px solid rgba(148,163,184,0.35);
41
+ position: relative;
42
+ overflow: hidden;
43
  }
44
+
45
+ .hero-title {
46
+ font-size: 1.9rem;
47
+ font-weight: 700;
48
+ background: linear-gradient(90deg, #f97316, #facc15, #22c55e, #38bdf8, #a855f7, #f97316);
49
+ background-size: 400% 100%;
50
+ -webkit-background-clip: text;
51
+ color: transparent;
52
+ animation: moveGradient 9s ease infinite;
53
  }
54
+
55
+ .hero-sub {
56
+ color: #9ca3af;
57
+ font-size: 0.95rem;
58
+ }
59
+
60
+ .hero-pill {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ gap: 6px;
64
+ padding: 3px 11px;
65
+ border-radius: 999px;
66
+ background: rgba(15,118,110,0.2);
67
+ border: 1px solid rgba(34,197,94,0.6);
68
+ font-size: 0.75rem;
69
+ color: #bbf7d0;
70
+ margin-right: 8px;
71
+ }
72
+
73
+ @keyframes moveGradient {
74
+ 0% { background-position: 0% 50%; }
75
+ 50% { background-position: 100% 50%; }
76
+ 100% { background-position: 0% 50%; }
77
  }
78
+
79
+ .glass {
80
+ background: radial-gradient(circle at top left, rgba(148,163,184,0.24), transparent 55%),
81
+ rgba(15,23,42,0.96);
82
  border-radius: 18px;
83
+ padding: 18px 18px 14px 18px;
84
+ border: 1px solid rgba(148,163,184,0.4);
85
+ box-shadow: 0 20px 40px rgba(15,23,42,0.6);
86
  }
87
 
88
+ .chip {
89
+ display:inline-block;
90
+ padding:4px 10px;
91
+ margin:3px 4px 3px 0;
92
+ border-radius:999px;
93
+ font-size:0.78rem;
94
+ background:rgba(59,130,246,0.14);
95
+ border:1px solid rgba(59,130,246,0.45);
96
+ color:#bfdbfe;
97
  }
98
+
99
+ .meter-label {
100
+ font-size: 0.85rem;
101
+ color: #9ca3af;
102
+ margin-bottom: 3px;
103
  }
104
+
105
+ .muted {
106
+ font-size: 0.8rem;
107
+ color: #6b7280;
 
108
  }
109
+
110
+ /* Tabs */
111
+ button[data-baseweb="tab"] {
112
+ background: transparent !important;
113
+ border-radius: 999px !important;
114
+ padding: 0.5rem 1rem !important;
115
+ margin-right: 0.4rem;
116
+ color: #9ca3af !important;
117
  }
118
+ button[data-baseweb="tab"][aria-selected="true"] {
119
+ background: rgba(59,130,246,0.2) !important;
120
+ color: #e5e7eb !important;
121
+ box-shadow: 0 0 0 1px rgba(59,130,246,0.7);
 
 
122
  }
123
+ </style>
124
+ """,
125
+ unsafe_allow_html=True
126
+ )
127
+
128
+ # ---------------------------
129
+ # Data & model (same logic)
130
+ # ---------------------------
131
+ def generate_dataset(n=400):
 
 
 
 
132
  np.random.seed(42)
 
133
  data = []
 
134
  for _ in range(n):
135
+ engine = np.random.choice([1.6, 2.0, 2.5, 3.0, 3.5, 5.0])
136
+ cyl = np.random.choice([4, 6, 8])
137
+ base_hp = int(engine * cyl * np.random.uniform(18, 22))
138
+
139
  intake = np.random.choice([0, 1, 2])
140
+ exhaust = np.random.choice([0, 1, 2])
141
+ induction = np.random.choice([0, 1, 2])
 
 
142
  fuel = np.random.choice([0, 1, 2, 3])
143
  tune = np.random.choice([0, 1, 2])
144
+ altitude = np.random.uniform(0, 2000)
145
+
146
+ hp_gain = (
147
+ intake * np.random.uniform(3, 10) +
148
+ exhaust * np.random.uniform(5, 20) +
149
+ induction * np.random.uniform(25, 100) +
150
+ tune * np.random.uniform(10, 35) +
151
+ fuel * np.random.uniform(2, 8) -
152
+ altitude * 0.01 +
153
+ np.random.uniform(-3, 3)
154
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
+ data.append([engine, cyl, base_hp, intake, exhaust,
157
+ induction, fuel, tune, altitude, hp_gain])
158
+
159
+ columns = [
160
+ "engine", "cyl", "base_hp", "intake", "exhaust",
161
+ "induction", "fuel", "tune", "altitude", "hp_gain"
162
+ ]
163
+ return pd.DataFrame(data, columns=columns)
164
+
165
+ df = generate_dataset()
166
+ X = df.drop("hp_gain", axis=1)
167
+ y = df["hp_gain"]
168
 
169
+ scaler = StandardScaler()
170
+ X_scaled = scaler.fit_transform(X)
171
+
172
+ model = KNeighborsRegressor(n_neighbors=5, weights="distance")
173
+ model.fit(X_scaled, y)
174
+
175
+ # ---------------------------
176
+ # HERO SECTION
177
+ # ---------------------------
178
+ st.markdown(
179
+ """
180
+ <div class="hero">
181
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:14px;">
182
+ <div>
183
+ <div class="hero-pill">βš™οΈ Powered by KNN + synthetic dyno data</div>
184
+ <div class="hero-title">Neural Tuner – Live Car Mod Performance Lab</div>
185
+ <p class="hero-sub">
186
+ Mix intakes, turbos, tunes & fuel in real time. Watch the HP jump,
187
+ the power-to-weight spike and your virtual build go absolutely feral. 🐺
188
+ </p>
189
+ </div>
190
+ <div style="
191
+ width:170px;height:110px;
192
+ border-radius:22px;
193
+ background:conic-gradient(from 220deg,
194
+ #22c55e, #38bdf8, #a855f7, #f97316, #facc15, #22c55e);
195
+ padding:2px;
196
+ ">
197
+ <div style="
198
+ width:100%;height:100%;
199
+ border-radius:19px;
200
+ background:radial-gradient(circle at 30% 0%, rgba(248,250,252,0.2), transparent 55%),
201
+ #020617;">
202
+ <div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;">
203
+ <div style="font-size:0.78rem;color:#9ca3af;">Live Build</div>
204
+ <div style="font-size:1.6rem;font-weight:700;color:#e5e7eb;">HP Lab</div>
205
+ <div style="font-size:0.75rem;color:#22c55e;">Realtime estimator</div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </div>
210
  </div>
211
+ """,
212
+ unsafe_allow_html=True
213
+ )
214
 
215
+ st.markdown("")
 
 
216
 
217
+ # ---------------------------
218
+ # Layout: left (controls) / right (dashboard)
219
+ # ---------------------------
220
+ left, right = st.columns([1.15, 1])
221
 
222
+ # ===== LEFT: TUNING CONTROLS =====
223
+ with left:
224
+ st.markdown('<div class="glass">', unsafe_allow_html=True)
225
+ st.subheader("πŸŽ›οΈ Tune Your Build")
226
+
227
+ tabs = st.tabs(["Core Specs", "Bolt-Ons", "Boost & Cooling", "ECU & Fuel"])
228
+
229
+ # --- Core specs tab ---
230
+ with tabs[0]:
231
+ col1, col2 = st.columns(2)
232
+ engine_disp_options = [0.8,1.0,1.2,1.4,1.6,1.8,2.0,2.2,2.4,2.5,
233
+ 2.8,3.0,3.2,3.5,4.0,4.4,5.0,6.0,7.0,8.0]
234
+ engine_disp = col1.selectbox("Engine Displacement (L)", engine_disp_options, index=engine_disp_options.index(2.0))
235
+ engine_layout = col2.selectbox("Engine Layout", ["I3","I4","I6","V6","V8","V10","V12"], index=2)
236
+
237
+ layout_to_cyl = {"I3":3,"I4":4,"I6":6,"V6":6,"V8":8,"V10":10,"V12":12}
238
+ cyl = layout_to_cyl[engine_layout]
239
+
240
+ base_hp = st.number_input(
241
+ "Base Horsepower (stock dyno)",
242
+ min_value=60, max_value=1400,
243
+ value=int(max(90, round(engine_disp * cyl * 20)))
244
+ )
245
+
246
+ col3, col4 = st.columns(2)
247
+ weight_kg = col3.number_input("Vehicle Weight (kg)", min_value=700, max_value=4000, value=1500)
248
+ weight_reduction = col4.slider("Weight Reduction (%)", 0, 40, 0)
249
+
250
+ # --- Bolt-ons tab ---
251
+ with tabs[1]:
252
+ col1, col2, col3 = st.columns(3)
253
+ intake = col1.selectbox("Intake", ["Stock", "Cold Air", "Performance"])
254
+ headers = col2.selectbox("Headers", ["Stock", "Shorty", "Long Tube"])
255
+ exhaust = col3.selectbox("Exhaust", ["Stock", "Cat-back", "Straight Pipe"])
256
+ exhaust_dia = st.slider("Exhaust Diameter (mm)", 40, 120, 60)
257
+
258
+ cam = st.selectbox("Cam Profile", ["Stock", "Stage 1 – Road", "Stage 2 – Aggressive", "Stage 3 – Race"])
259
+ intake_manifold = st.selectbox("Intake Manifold", ["Stock", "High-flow", "Individual throttle bodies"])
260
+
261
+ # --- Boost & cooling tab ---
262
+ with tabs[2]:
263
+ induction = st.selectbox("Forced Induction Setup", ["None", "Turbo", "Twin-Turbo", "Supercharger", "Twincharged"])
264
+ boost_psi = st.slider("Target Boost (psi)", 0, 40, 10)
265
+ turbo_size = st.selectbox("Turbo Size", ["N/A", "Small 45-55mm", "Medium 56-65mm", "Big 66-75mm", "XL 76mm+"])
266
+ intercooler = st.selectbox("Intercooler Type", ["None", "Air-to-Air", "Air-to-Water", "Front-mount High-Flow"])
267
+ meth = st.checkbox("Methanol Injection Kit", value=False)
268
+
269
+ # --- ECU & fuel tab ---
270
+ with tabs[3]:
271
+ tune = st.selectbox("ECU Tune Level", ["None", "Mild Street", "Stage 1", "Stage 2", "Kill Mode"])
272
+ fuel = st.selectbox("Fuel Type", ["87", "91", "93", "E85 / Race Blend"])
273
+ altitude = st.slider("Altitude (meters)", 0, 3500, 200)
274
+ traction_mode = st.radio("Traction Mode Vibe", ["Daily", "Spirited", "Track / Drag"], horizontal=True)
275
+
276
+ st.markdown(
277
+ '<p class="muted" style="margin-top:8px;">Numbers are synthetic; this is a dyno-inspired playground, not a tuning bible. πŸ§ͺ</p>',
278
+ unsafe_allow_html=True
279
+ )
280
+ st.markdown('</div>', unsafe_allow_html=True)
281
+
282
+ # ===== MODEL INPUT & CALC =====
283
+
284
+ # maps for model
285
+ intake_map = {"Stock":0, "Cold Air":1, "Performance":2}
286
+ exhaust_map = {"Stock":0, "Cat-back":1, "Straight Pipe":2}
287
+ induction_model_map = {"None":0, "Turbo":1, "Twin-Turbo":1, "Supercharger":2, "Twincharged":2}
288
+ fuel_map = {"87":0, "91":1, "93":2, "E85 / Race Blend":3}
289
+ tune_base_map = {"None":0, "Mild Street":1, "Stage 1":1, "Stage 2":2, "Kill Mode":2}
290
+
291
+ input_for_model = np.array([[
292
+ engine_disp,
293
+ cyl,
294
+ base_hp,
295
+ intake_map.get(intake,0),
296
+ exhaust_map.get(exhaust,0),
297
+ induction_model_map.get(induction,0),
298
+ fuel_map.get(fuel,0),
299
+ tune_base_map.get(tune,0),
300
+ altitude
301
+ ]])
302
+
303
+ input_scaled = scaler.transform(input_for_model)
304
+ pred_base = float(model.predict(input_scaled)[0])
305
+
306
+ # --- heuristic extras ---
307
+ cam_gain_map = {
308
+ "Stock":0.0,
309
+ "Stage 1 – Road":6.0,
310
+ "Stage 2 – Aggressive":12.0,
311
+ "Stage 3 – Race":20.0
312
+ }
313
+ headers_gain_map = {"Stock":0.0, "Shorty":5.0, "Long Tube":9.0}
314
+ intake_man_gain_map = {
315
+ "Stock":0.0,
316
+ "High-flow":5.0,
317
+ "Individual throttle bodies":10.0
318
+ }
319
+ intercooler_gain_map = {
320
+ "None":0.0,
321
+ "Air-to-Air":4.0,
322
+ "Air-to-Water":6.5,
323
+ "Front-mount High-Flow":9.0
324
+ }
325
+ turbo_size_map = {
326
+ "N/A":0.0,
327
+ "Small 45-55mm":5.0,
328
+ "Medium 56-65mm":12.0,
329
+ "Big 66-75mm":20.0,
330
+ "XL 76mm+":28.0
331
+ }
332
+
333
+ cam_gain = cam_gain_map.get(cam, 0.0)
334
+ headers_gain = headers_gain_map.get(headers, 0.0)
335
+ intake_man_gain = intake_man_gain_map.get(intake_manifold, 0.0)
336
+ intercooler_gain = intercooler_gain_map.get(intercooler, 0.0)
337
+ turbo_size_gain = turbo_size_map.get(turbo_size, 0.0)
338
+
339
+ if induction in ["Turbo", "Twin-Turbo"]:
340
+ boost_gain = boost_psi * 1.8
341
+ elif induction in ["Supercharger", "Twincharged"]:
342
+ boost_gain = boost_psi * 1.4
343
+ else:
344
+ boost_gain = 0.0
345
+
346
+ meth_gain = 12.0 if meth else 0.0
347
+ exhaust_dia_gain = max(0.0, (exhaust_dia - 55) * 0.1)
348
+
349
+ extra_gain = (
350
+ cam_gain +
351
+ headers_gain +
352
+ intake_man_gain +
353
+ intercooler_gain +
354
+ turbo_size_gain +
355
+ boost_gain * 0.9 +
356
+ meth_gain +
357
+ exhaust_dia_gain
358
+ )
359
+
360
+ # traction / vibe adjusts "usable feel"
361
+ traction_multiplier = {"Daily":0.9, "Spirited":1.0, "Track / Drag":1.05}[traction_mode]
362
+
363
+ pred_total_gain = (pred_base + extra_gain) * traction_multiplier
364
+ pred_total_gain = max(pred_total_gain, -5.0) # clamp
365
+ new_hp = max(base_hp + pred_total_gain, 40.0)
366
+
367
+ effective_weight = weight_kg * (1 - weight_reduction / 100.0)
368
+ hp_per_ton = new_hp / (effective_weight / 1000.0)
369
+
370
+ # normalized hype level 0–100
371
+ hype_raw = np.clip(pred_total_gain / 120 * 100, 0, 100)
372
+ hype_level = int(hype_raw)
373
+
374
+ # verdict text
375
+ if hype_level < 20:
376
+ verdict = "Sleeper grocery getter πŸš™"
377
+ elif hype_level < 40:
378
+ verdict = "Respectable street build πŸŒƒ"
379
+ elif hype_level < 70:
380
+ verdict = "Serious weekend weapon βš”οΈ"
381
+ else:
382
+ verdict = "Full send, tyres cry for mercy 🏁"
383
+
384
+ # ===== RIGHT: DASHBOARD =====
385
+ with right:
386
+ st.markdown('<div class="glass">', unsafe_allow_html=True)
387
+ st.subheader("πŸ’₯ Build Outcome")
388
+
389
+ m1, m2, m3 = st.columns(3)
390
+ m1.metric("Estimated HP Gain", f"{pred_total_gain:.1f} HP")
391
+ m2.metric("New Output", f"{new_hp:.1f} HP")
392
+ m3.metric("HP per Ton", f"{hp_per_ton:.1f}")
393
+
394
+ st.markdown("")
395
+ st.markdown('<div class="meter-label">Hype Meter (relative craziness of this build)</div>', unsafe_allow_html=True)
396
+ st.progress(hype_level)
397
+
398
+ st.markdown(f"**Verdict:** {verdict}")
399
+
400
+ # small bar chart: stock vs tuned
401
+ st.markdown("")
402
+ st.bar_chart(
403
+ pd.DataFrame(
404
+ {"Horsepower": [base_hp, new_hp]},
405
+ index=["Stock", "Tuned"]
406
+ )
407
+ )
408
+
409
+ # chips summary
410
+ st.markdown(
411
+ f"""
412
+ <div style="margin-top:6px;">
413
+ <span class="chip">{engine_disp}L {engine_layout}</span>
414
+ <span class="chip">Weight: {weight_kg} kg β–Έ βˆ’{weight_reduction}%</span>
415
+ <span class="chip">Intake: {intake}</span>
416
+ <span class="chip">Headers: {headers}</span>
417
+ <span class="chip">Exhaust: {exhaust} ({exhaust_dia}mm)</span><br/>
418
+ <span class="chip">Induction: {induction} @ {boost_psi} psi</span>
419
+ <span class="chip">IC: {intercooler}</span>
420
+ <span class="chip">Tune: {tune}</span>
421
+ <span class="chip">Fuel: {fuel}</span>
422
  </div>
423
+ """,
424
+ unsafe_allow_html=True
425
+ )
426
+
427
+ st.markdown("---")
428
+
429
+ # Contribution breakdown (no Styler.hide_index)
430
+ breakdown = pd.DataFrame({
431
+ "Component": [
432
+ "Model base (from dataset)",
433
+ "Cam profile",
434
+ "Headers",
435
+ "Intake manifold",
436
+ "Intercooler",
437
+ "Turbo size",
438
+ "Boost (net)",
439
+ "Meth kit",
440
+ "Exhaust diameter tweak"
441
+ ],
442
+ "Approx HP": [
443
+ pred_base,
444
+ cam_gain,
445
+ headers_gain,
446
+ intake_man_gain,
447
+ intercooler_gain,
448
+ turbo_size_gain,
449
+ boost_gain * 0.9,
450
+ meth_gain,
451
+ exhaust_dia_gain
452
+ ]
453
+ })
454
+
455
+ st.caption("πŸ”¬ Where is that extra power coming from?")
456
+ st.dataframe(breakdown, use_container_width=True, height=260)
457
+
458
+ st.markdown(
459
+ '<p class="muted" style="margin-top:8px;">Model: distance-weighted KNN on synthetic dyno-style data + extra heuristic math for advanced mods.</p>',
460
+ unsafe_allow_html=True
461
+ )
462
+ st.markdown('</div>', unsafe_allow_html=True)
463
+
464
+ # ===== FOOTER =====
465
+ st.markdown(
466
+ """
467
+ <div style="text-align:center;margin-top:10px;" class="muted">
468
+ Built for fun, learning & ridiculous builds – drop this in a Space and watch car nerds lose it. πŸ”§πŸ”₯
469
+ </div>
470
+ """,
471
+ unsafe_allow_html=True
472
+ )