NeuralGearheads commited on
Commit
9c6d73d
·
verified ·
1 Parent(s): 945ffe8

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +289 -60
src/streamlit_app.py CHANGED
@@ -14,7 +14,7 @@ st.set_page_config(
14
  )
15
 
16
  # ---------------------------
17
- # Global CSS crazy but clean
18
  # ---------------------------
19
  st.markdown(
20
  """
@@ -25,8 +25,6 @@ st.markdown(
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
 
@@ -41,7 +39,6 @@ st.markdown(
41
  position: relative;
42
  overflow: hidden;
43
  }
44
-
45
  .hero-title {
46
  font-size: 1.9rem;
47
  font-weight: 700;
@@ -51,12 +48,10 @@ st.markdown(
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;
@@ -69,7 +64,6 @@ st.markdown(
69
  color: #bbf7d0;
70
  margin-right: 8px;
71
  }
72
-
73
  @keyframes moveGradient {
74
  0% { background-position: 0% 50%; }
75
  50% { background-position: 100% 50%; }
@@ -101,7 +95,6 @@ st.markdown(
101
  color: #9ca3af;
102
  margin-bottom: 3px;
103
  }
104
-
105
  .muted {
106
  font-size: 0.8rem;
107
  color: #6b7280;
@@ -120,13 +113,83 @@ st.markdown(
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)
@@ -173,18 +236,114 @@ 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="
@@ -200,9 +359,9 @@ st.markdown(
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>
@@ -215,24 +374,43 @@ st.markdown(
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]
@@ -240,38 +418,80 @@ with left:
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>',
@@ -279,9 +499,8 @@ with left:
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}
@@ -303,7 +522,6 @@ input_for_model = np.array([[
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,
@@ -357,21 +575,18 @@ extra_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:
@@ -381,7 +596,10 @@ elif hype_level < 70:
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")
@@ -391,13 +609,26 @@ with right:
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(
@@ -406,7 +637,6 @@ with right:
406
  )
407
  )
408
 
409
- # chips summary
410
  st.markdown(
411
  f"""
412
  <div style="margin-top:6px;">
@@ -426,7 +656,6 @@ with right:
426
 
427
  st.markdown("---")
428
 
429
- # Contribution breakdown (no Styler.hide_index)
430
  breakdown = pd.DataFrame({
431
  "Component": [
432
  "Model base (from dataset)",
@@ -456,16 +685,16 @@ with right:
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
 
14
  )
15
 
16
  # ---------------------------
17
+ # Global CSS (mac-neon + rev gauge)
18
  # ---------------------------
19
  st.markdown(
20
  """
 
25
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text",
26
  "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
27
  }
 
 
28
  [data-testid="stHeader"] { background: transparent; }
29
  [data-testid="stToolbar"] { display: none; }
30
 
 
39
  position: relative;
40
  overflow: hidden;
41
  }
 
42
  .hero-title {
43
  font-size: 1.9rem;
44
  font-weight: 700;
 
48
  color: transparent;
49
  animation: moveGradient 9s ease infinite;
50
  }
 
51
  .hero-sub {
52
  color: #9ca3af;
53
  font-size: 0.95rem;
54
  }
 
55
  .hero-pill {
56
  display: inline-flex;
57
  align-items: center;
 
64
  color: #bbf7d0;
65
  margin-right: 8px;
66
  }
 
67
  @keyframes moveGradient {
68
  0% { background-position: 0% 50%; }
69
  50% { background-position: 100% 50%; }
 
95
  color: #9ca3af;
96
  margin-bottom: 3px;
97
  }
 
98
  .muted {
99
  font-size: 0.8rem;
100
  color: #6b7280;
 
113
  color: #e5e7eb !important;
114
  box-shadow: 0 0 0 1px rgba(59,130,246,0.7);
115
  }
116
+
117
+ /* Rev limiter gauge */
118
+ .rev-wrap {
119
+ display:flex;
120
+ justify-content:center;
121
+ align-items:center;
122
+ margin-top:4px;
123
+ margin-bottom:4px;
124
+ }
125
+ .rev-gauge {
126
+ position:relative;
127
+ width:170px;
128
+ height:90px;
129
+ border-radius:170px 170px 14px 14px;
130
+ background: radial-gradient(circle at 50% 120%, #991b1b, transparent 60%),
131
+ radial-gradient(circle at 0% 0%, rgba(59,130,246,0.4), transparent 60%),
132
+ #020617;
133
+ overflow:hidden;
134
+ border:1px solid rgba(148,163,184,0.5);
135
+ box-shadow: 0 12px 30px rgba(0,0,0,0.8);
136
+ }
137
+ .rev-arc {
138
+ position:absolute;
139
+ inset:10px 10px auto 10px;
140
+ height:62px;
141
+ border-radius:999px;
142
+ border-top:6px solid rgba(249,250,251,0.16);
143
+ border-left:6px solid rgba(34,197,94,0.75);
144
+ border-right:6px solid rgba(220,38,38,0.9);
145
+ border-bottom:none;
146
+ }
147
+ .rev-needle {
148
+ --base-angle: -40deg;
149
+ position:absolute;
150
+ left:50%;
151
+ bottom:8px;
152
+ width:3px;
153
+ height:68px;
154
+ background: linear-gradient(to top, #fecaca, #f97316);
155
+ border-radius:999px;
156
+ transform-origin:50% 90%;
157
+ animation: revBounce 0.6s ease-in-out infinite alternate;
158
+ }
159
+ .rev-center {
160
+ position:absolute;
161
+ left:50%;
162
+ bottom:4px;
163
+ transform:translateX(-50%);
164
+ width:22px;
165
+ height:22px;
166
+ border-radius:999px;
167
+ background:#020617;
168
+ border:2px solid #9ca3af;
169
+ display:flex;
170
+ align-items:center;
171
+ justify-content:center;
172
+ font-size:0.55rem;
173
+ color:#9ca3af;
174
+ }
175
+ .rev-label {
176
+ position:absolute;
177
+ right:8px;
178
+ bottom:6px;
179
+ font-size:0.7rem;
180
+ color:#fecaca;
181
+ }
182
+ @keyframes revBounce {
183
+ from { transform: rotate(var(--base-angle)); }
184
+ to { transform: rotate(calc(var(--base-angle) + 14deg)); }
185
+ }
186
  </style>
187
  """,
188
  unsafe_allow_html=True
189
  )
190
 
191
  # ---------------------------
192
+ # Data & model
193
  # ---------------------------
194
  def generate_dataset(n=400):
195
  np.random.seed(42)
 
236
  model.fit(X_scaled, y)
237
 
238
  # ---------------------------
239
+ # PRESET CALLBACKS (use session_state)
240
+ # ---------------------------
241
+ def preset_daily():
242
+ s = st.session_state
243
+ s.engine_disp = 1.6
244
+ s.engine_layout = "I4"
245
+ s.base_hp = 120
246
+ s.weight_kg = 1350
247
+ s.weight_reduction = 0
248
+ s.intake = "Stock"
249
+ s.headers = "Stock"
250
+ s.exhaust = "Stock"
251
+ s.exhaust_dia = 50
252
+ s.cam = "Stock"
253
+ s.intake_manifold = "Stock"
254
+ s.induction = "None"
255
+ s.boost_psi = 0
256
+ s.turbo_size = "N/A"
257
+ s.intercooler = "None"
258
+ s.meth = False
259
+ s.tune = "None"
260
+ s.fuel = "87"
261
+ s.altitude = 200
262
+ s.traction_mode = "Daily"
263
+
264
+ def preset_street():
265
+ s = st.session_state
266
+ s.engine_disp = 2.0
267
+ s.engine_layout = "I4"
268
+ s.base_hp = 190
269
+ s.weight_kg = 1400
270
+ s.weight_reduction = 5
271
+ s.intake = "Cold Air"
272
+ s.headers = "Shorty"
273
+ s.exhaust = "Cat-back"
274
+ s.exhaust_dia = 60
275
+ s.cam = "Stage 1 – Road"
276
+ s.intake_manifold = "High-flow"
277
+ s.induction = "Turbo"
278
+ s.boost_psi = 12
279
+ s.turbo_size = "Small 45-55mm"
280
+ s.intercooler = "Air-to-Air"
281
+ s.meth = False
282
+ s.tune = "Stage 1"
283
+ s.fuel = "93"
284
+ s.altitude = 150
285
+ s.traction_mode = "Spirited"
286
+
287
+ def preset_drag():
288
+ s = st.session_state
289
+ s.engine_disp = 5.0
290
+ s.engine_layout = "V8"
291
+ s.base_hp = 420
292
+ s.weight_kg = 1500
293
+ s.weight_reduction = 18
294
+ s.intake = "Performance"
295
+ s.headers = "Long Tube"
296
+ s.exhaust = "Straight Pipe"
297
+ s.exhaust_dia = 90
298
+ s.cam = "Stage 2 – Aggressive"
299
+ s.intake_manifold = "High-flow"
300
+ s.induction = "Twin-Turbo"
301
+ s.boost_psi = 24
302
+ s.turbo_size = "Big 66-75mm"
303
+ s.intercooler = "Front-mount High-Flow"
304
+ s.meth = True
305
+ s.tune = "Kill Mode"
306
+ s.fuel = "E85 / Race Blend"
307
+ s.altitude = 100
308
+ s.traction_mode = "Track / Drag"
309
+
310
+ def preset_max():
311
+ s = st.session_state
312
+ s.engine_disp = 6.0
313
+ s.engine_layout = "V12"
314
+ s.base_hp = 650
315
+ s.weight_kg = 1400
316
+ s.weight_reduction = 25
317
+ s.intake = "Performance"
318
+ s.headers = "Long Tube"
319
+ s.exhaust = "Straight Pipe"
320
+ s.exhaust_dia = 100
321
+ s.cam = "Stage 3 – Race"
322
+ s.intake_manifold = "Individual throttle bodies"
323
+ s.induction = "Twincharged"
324
+ s.boost_psi = 30
325
+ s.turbo_size = "XL 76mm+"
326
+ s.intercooler = "Front-mount High-Flow"
327
+ s.meth = True
328
+ s.tune = "Kill Mode"
329
+ s.fuel = "E85 / Race Blend"
330
+ s.altitude = 0
331
+ s.traction_mode = "Track / Drag"
332
+
333
+ # ---------------------------
334
+ # HERO
335
  # ---------------------------
336
  st.markdown(
337
  """
338
  <div class="hero">
339
  <div style="display:flex;justify-content:space-between;align-items:center;gap:14px;">
340
  <div>
341
+ <div class="hero-pill">⚙️ KNN model + synthetic dyno data</div>
342
+ <div class="hero-pill">🎛️ Live tuning playground</div>
343
+ <div class="hero-title">Neural Tuner – Car Mod Performance Lab</div>
344
  <p class="hero-sub">
345
+ Mash intakes, boost, tunes and fuel in real time.
346
+ Watch horsepower climb and the rev limiter dance. Built to make car nerds go feral. 🐺
347
  </p>
348
  </div>
349
  <div style="
 
359
  background:radial-gradient(circle at 30% 0%, rgba(248,250,252,0.2), transparent 55%),
360
  #020617;">
361
  <div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;">
362
+ <div style="font-size:0.78rem;color:#9ca3af;">Virtual</div>
363
+ <div style="font-size:1.6rem;font-weight:700;color:#e5e7eb;">Dyno</div>
364
+ <div style="font-size:0.75rem;color:#22c55e;">No hardware, all chaos</div>
365
  </div>
366
  </div>
367
  </div>
 
374
  st.markdown("")
375
 
376
  # ---------------------------
377
+ # Layout
378
  # ---------------------------
379
  left, right = st.columns([1.15, 1])
380
 
381
+ # ========= LEFT SIDE =========
382
  with left:
383
  st.markdown('<div class="glass">', unsafe_allow_html=True)
384
  st.subheader("🎛️ Tune Your Build")
385
 
386
+ # Preset buttons
387
+ bcol1, bcol2, bcol3, bcol4 = st.columns(4)
388
+ bcol1.button("🚙 Daily Driver", on_click=preset_daily)
389
+ bcol2.button("🌃 Street Fun", on_click=preset_street)
390
+ bcol3.button("🏁 Drag Strip", on_click=preset_drag)
391
+ bcol4.button("🔥 Max Attack", on_click=preset_max)
392
+
393
+ st.markdown("")
394
+
395
  tabs = st.tabs(["Core Specs", "Bolt-Ons", "Boost & Cooling", "ECU & Fuel"])
396
 
397
+ # --- Core specs ---
398
  with tabs[0]:
399
  col1, col2 = st.columns(2)
400
  engine_disp_options = [0.8,1.0,1.2,1.4,1.6,1.8,2.0,2.2,2.4,2.5,
401
  2.8,3.0,3.2,3.5,4.0,4.4,5.0,6.0,7.0,8.0]
402
+ engine_disp = col1.selectbox(
403
+ "Engine Displacement (L)",
404
+ engine_disp_options,
405
+ index=engine_disp_options.index(2.0),
406
+ key="engine_disp"
407
+ )
408
+ engine_layout = col2.selectbox(
409
+ "Engine Layout",
410
+ ["I3","I4","I6","V6","V8","V10","V12"],
411
+ index=2,
412
+ key="engine_layout"
413
+ )
414
 
415
  layout_to_cyl = {"I3":3,"I4":4,"I6":6,"V6":6,"V8":8,"V10":10,"V12":12}
416
  cyl = layout_to_cyl[engine_layout]
 
418
  base_hp = st.number_input(
419
  "Base Horsepower (stock dyno)",
420
  min_value=60, max_value=1400,
421
+ value=int(max(90, round(engine_disp * cyl * 20))),
422
+ key="base_hp"
423
  )
424
 
425
  col3, col4 = st.columns(2)
426
+ weight_kg = col3.number_input(
427
+ "Vehicle Weight (kg)",
428
+ min_value=700, max_value=4000, value=1500,
429
+ key="weight_kg"
430
+ )
431
+ weight_reduction = col4.slider(
432
+ "Weight Reduction (%)",
433
+ 0, 40, 0,
434
+ key="weight_reduction"
435
+ )
436
 
437
+ # --- Bolt-ons ---
438
  with tabs[1]:
439
  col1, col2, col3 = st.columns(3)
440
+ intake = col1.selectbox("Intake", ["Stock", "Cold Air", "Performance"], key="intake")
441
+ headers = col2.selectbox("Headers", ["Stock", "Shorty", "Long Tube"], key="headers")
442
+ exhaust = col3.selectbox("Exhaust", ["Stock", "Cat-back", "Straight Pipe"], key="exhaust")
443
+ exhaust_dia = st.slider("Exhaust Diameter (mm)", 40, 120, 60, key="exhaust_dia")
444
+
445
+ cam = st.selectbox(
446
+ "Cam Profile",
447
+ ["Stock", "Stage 1 – Road", "Stage 2 – Aggressive", "Stage 3 – Race"],
448
+ key="cam"
449
+ )
450
+ intake_manifold = st.selectbox(
451
+ "Intake Manifold",
452
+ ["Stock", "High-flow", "Individual throttle bodies"],
453
+ key="intake_manifold"
454
+ )
455
 
456
+ # --- Boost & cooling ---
457
  with tabs[2]:
458
+ induction = st.selectbox(
459
+ "Forced Induction Setup",
460
+ ["None", "Turbo", "Twin-Turbo", "Supercharger", "Twincharged"],
461
+ key="induction"
462
+ )
463
+ boost_psi = st.slider("Target Boost (psi)", 0, 40, 10, key="boost_psi")
464
+ turbo_size = st.selectbox(
465
+ "Turbo Size",
466
+ ["N/A", "Small 45-55mm", "Medium 56-65mm", "Big 66-75mm", "XL 76mm+"],
467
+ key="turbo_size"
468
+ )
469
+ intercooler = st.selectbox(
470
+ "Intercooler Type",
471
+ ["None", "Air-to-Air", "Air-to-Water", "Front-mount High-Flow"],
472
+ key="intercooler"
473
+ )
474
+ meth = st.checkbox("Methanol Injection Kit", value=False, key="meth")
475
 
476
+ # --- ECU & fuel ---
477
  with tabs[3]:
478
+ tune = st.selectbox(
479
+ "ECU Tune Level",
480
+ ["None", "Mild Street", "Stage 1", "Stage 2", "Kill Mode"],
481
+ key="tune"
482
+ )
483
+ fuel = st.selectbox(
484
+ "Fuel Type",
485
+ ["87", "91", "93", "E85 / Race Blend"],
486
+ key="fuel"
487
+ )
488
+ altitude = st.slider("Altitude (meters)", 0, 3500, 200, key="altitude")
489
+ traction_mode = st.radio(
490
+ "Traction Mode Vibe",
491
+ ["Daily", "Spirited", "Track / Drag"],
492
+ horizontal=True,
493
+ key="traction_mode"
494
+ )
495
 
496
  st.markdown(
497
  '<p class="muted" style="margin-top:8px;">Numbers are synthetic; this is a dyno-inspired playground, not a tuning bible. 🧪</p>',
 
499
  )
500
  st.markdown('</div>', unsafe_allow_html=True)
501
 
502
+ # ========= MODEL & CALCS =========
503
 
 
504
  intake_map = {"Stock":0, "Cold Air":1, "Performance":2}
505
  exhaust_map = {"Stock":0, "Cat-back":1, "Straight Pipe":2}
506
  induction_model_map = {"None":0, "Turbo":1, "Twin-Turbo":1, "Supercharger":2, "Twincharged":2}
 
522
  input_scaled = scaler.transform(input_for_model)
523
  pred_base = float(model.predict(input_scaled)[0])
524
 
 
525
  cam_gain_map = {
526
  "Stock":0.0,
527
  "Stage 1 – Road":6.0,
 
575
  exhaust_dia_gain
576
  )
577
 
 
578
  traction_multiplier = {"Daily":0.9, "Spirited":1.0, "Track / Drag":1.05}[traction_mode]
579
 
580
  pred_total_gain = (pred_base + extra_gain) * traction_multiplier
581
+ pred_total_gain = max(pred_total_gain, -5.0)
582
  new_hp = max(base_hp + pred_total_gain, 40.0)
583
 
584
  effective_weight = weight_kg * (1 - weight_reduction / 100.0)
585
  hp_per_ton = new_hp / (effective_weight / 1000.0)
586
 
 
587
  hype_raw = np.clip(pred_total_gain / 120 * 100, 0, 100)
588
  hype_level = int(hype_raw)
589
 
 
590
  if hype_level < 20:
591
  verdict = "Sleeper grocery getter 🚙"
592
  elif hype_level < 40:
 
596
  else:
597
  verdict = "Full send, tyres cry for mercy 🏁"
598
 
599
+ # compute base angle for rev gauge (-90deg to +90deg)
600
+ angle_deg = -90 + (hype_level / 100.0) * 180
601
+
602
+ # ========= RIGHT SIDE =========
603
  with right:
604
  st.markdown('<div class="glass">', unsafe_allow_html=True)
605
  st.subheader("💥 Build Outcome")
 
609
  m2.metric("New Output", f"{new_hp:.1f} HP")
610
  m3.metric("HP per Ton", f"{hp_per_ton:.1f}")
611
 
612
+ # Animated rev limiter gauge
613
+ st.markdown('<div class="meter-label">Rev Limiter Vibes</div>', unsafe_allow_html=True)
614
+ st.markdown(
615
+ f"""
616
+ <div class="rev-wrap">
617
+ <div class="rev-gauge">
618
+ <div class="rev-arc"></div>
619
+ <div class="rev-needle" style="--base-angle:{angle_deg}deg;"></div>
620
+ <div class="rev-center">RPM</div>
621
+ <div class="rev-label">REDLINE</div>
622
+ </div>
623
+ </div>
624
+ """,
625
+ unsafe_allow_html=True
626
+ )
627
+
628
  st.markdown('<div class="meter-label">Hype Meter (relative craziness of this build)</div>', unsafe_allow_html=True)
629
  st.progress(hype_level)
 
630
  st.markdown(f"**Verdict:** {verdict}")
631
 
 
632
  st.markdown("")
633
  st.bar_chart(
634
  pd.DataFrame(
 
637
  )
638
  )
639
 
 
640
  st.markdown(
641
  f"""
642
  <div style="margin-top:6px;">
 
656
 
657
  st.markdown("---")
658
 
 
659
  breakdown = pd.DataFrame({
660
  "Component": [
661
  "Model base (from dataset)",
 
685
  st.dataframe(breakdown, use_container_width=True, height=260)
686
 
687
  st.markdown(
688
+ '<p class="muted" style="margin-top:8px;">Model: distance-weighted KNN on synthetic dyno-style data + heuristic math for advanced mods.</p>',
689
  unsafe_allow_html=True
690
  )
691
  st.markdown('</div>', unsafe_allow_html=True)
692
 
693
+ # ========= FOOTER =========
694
  st.markdown(
695
  """
696
  <div style="text-align:center;margin-top:10px;" class="muted">
697
+ Presets + rev limiter + neon dyno… if this doesn’t make people go mad, we can push it even further. 🔧🔥
698
  </div>
699
  """,
700
  unsafe_allow_html=True