NeuralGearheads commited on
Commit
8d043f9
·
verified ·
1 Parent(s): b9e0146

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +317 -666
src/streamlit_app.py CHANGED
@@ -1,701 +1,352 @@
1
  import streamlit as st
2
- import pandas as pd
3
- import numpy as np
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 (mac-neon + rev gauge)
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
- [data-testid="stHeader"] { background: transparent; }
29
- [data-testid="stToolbar"] { display: none; }
30
-
31
- .hero {
32
- border-radius: 24px;
33
- padding: 18px 22px;
34
- background: radial-gradient(circle at top left, rgba(96,165,250,0.35), transparent 55%),
35
- radial-gradient(circle at bottom right, rgba(236,72,153,0.35), transparent 55%),
36
- rgba(15,23,42,0.94);
37
- box-shadow: 0 25px 60px rgba(15,23,42,0.9);
38
- border: 1px solid rgba(148,163,184,0.35);
39
- position: relative;
40
- overflow: hidden;
41
- }
42
- .hero-title {
43
- font-size: 1.9rem;
44
- font-weight: 700;
45
- background: linear-gradient(90deg, #f97316, #facc15, #22c55e, #38bdf8, #a855f7, #f97316);
46
- background-size: 400% 100%;
47
- -webkit-background-clip: text;
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;
58
- gap: 6px;
59
- padding: 3px 11px;
60
- border-radius: 999px;
61
- background: rgba(15,118,110,0.2);
62
- border: 1px solid rgba(34,197,94,0.6);
63
- font-size: 0.75rem;
64
- color: #bbf7d0;
65
- margin-right: 8px;
66
- }
67
- @keyframes moveGradient {
68
- 0% { background-position: 0% 50%; }
69
- 50% { background-position: 100% 50%; }
70
- 100% { background-position: 0% 50%; }
71
- }
72
-
73
- .glass {
74
- background: radial-gradient(circle at top left, rgba(148,163,184,0.24), transparent 55%),
75
- rgba(15,23,42,0.96);
76
- border-radius: 18px;
77
- padding: 18px 18px 14px 18px;
78
- border: 1px solid rgba(148,163,184,0.4);
79
- box-shadow: 0 20px 40px rgba(15,23,42,0.6);
80
- }
81
-
82
- .chip {
83
- display:inline-block;
84
- padding:4px 10px;
85
- margin:3px 4px 3px 0;
86
- border-radius:999px;
87
- font-size:0.78rem;
88
- background:rgba(59,130,246,0.14);
89
- border:1px solid rgba(59,130,246,0.45);
90
- color:#bfdbfe;
91
- }
92
-
93
- .meter-label {
94
- font-size: 0.85rem;
95
- color: #9ca3af;
96
- margin-bottom: 3px;
97
- }
98
- .muted {
99
- font-size: 0.8rem;
100
- color: #6b7280;
101
- }
102
-
103
- /* Tabs */
104
- button[data-baseweb="tab"] {
105
- background: transparent !important;
106
- border-radius: 999px !important;
107
- padding: 0.5rem 1rem !important;
108
- margin-right: 0.4rem;
109
- color: #9ca3af !important;
110
- }
111
- button[data-baseweb="tab"][aria-selected="true"] {
112
- background: rgba(59,130,246,0.2) !important;
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)
196
- data = []
197
- for _ in range(n):
198
- engine = np.random.choice([1.6, 2.0, 2.5, 3.0, 3.5, 5.0])
199
- cyl = np.random.choice([4, 6, 8])
200
- base_hp = int(engine * cyl * np.random.uniform(18, 22))
201
-
202
- intake = np.random.choice([0, 1, 2])
203
- exhaust = np.random.choice([0, 1, 2])
204
- induction = np.random.choice([0, 1, 2])
205
- fuel = np.random.choice([0, 1, 2, 3])
206
- tune = np.random.choice([0, 1, 2])
207
- altitude = np.random.uniform(0, 2000)
208
-
209
- hp_gain = (
210
- intake * np.random.uniform(3, 10) +
211
- exhaust * np.random.uniform(5, 20) +
212
- induction * np.random.uniform(25, 100) +
213
- tune * np.random.uniform(10, 35) +
214
- fuel * np.random.uniform(2, 8) -
215
- altitude * 0.01 +
216
- np.random.uniform(-3, 3)
217
- )
218
 
219
- data.append([engine, cyl, base_hp, intake, exhaust,
220
- induction, fuel, tune, altitude, hp_gain])
221
-
222
- columns = [
223
- "engine", "cyl", "base_hp", "intake", "exhaust",
224
- "induction", "fuel", "tune", "altitude", "hp_gain"
225
- ]
226
- return pd.DataFrame(data, columns=columns)
227
-
228
- df = generate_dataset()
229
- X = df.drop("hp_gain", axis=1)
230
- y = df["hp_gain"]
231
-
232
- scaler = StandardScaler()
233
- X_scaled = scaler.fit_transform(X)
234
-
235
- model = KNeighborsRegressor(n_neighbors=5, weights="distance")
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="
350
- width:170px;height:110px;
351
- border-radius:22px;
352
- background:conic-gradient(from 220deg,
353
- #22c55e, #38bdf8, #a855f7, #f97316, #facc15, #22c55e);
354
- padding:2px;
355
- ">
356
- <div style="
357
- width:100%;height:100%;
358
- border-radius:19px;
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>
368
- </div>
369
- </div>
370
- """,
371
- unsafe_allow_html=True
372
- )
373
 
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]
417
-
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>',
498
- unsafe_allow_html=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}
507
- fuel_map = {"87":0, "91":1, "93":2, "E85 / Race Blend":3}
508
- tune_base_map = {"None":0, "Mild Street":1, "Stage 1":1, "Stage 2":2, "Kill Mode":2}
509
-
510
- input_for_model = np.array([[
511
- engine_disp,
512
- cyl,
513
- base_hp,
514
- intake_map.get(intake,0),
515
- exhaust_map.get(exhaust,0),
516
- induction_model_map.get(induction,0),
517
- fuel_map.get(fuel,0),
518
- tune_base_map.get(tune,0),
519
- altitude
520
- ]])
521
-
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,
528
- "Stage 2 – Aggressive":12.0,
529
- "Stage 3 – Race":20.0
530
- }
531
- headers_gain_map = {"Stock":0.0, "Shorty":5.0, "Long Tube":9.0}
532
- intake_man_gain_map = {
533
- "Stock":0.0,
534
- "High-flow":5.0,
535
- "Individual throttle bodies":10.0
536
- }
537
- intercooler_gain_map = {
538
- "None":0.0,
539
- "Air-to-Air":4.0,
540
- "Air-to-Water":6.5,
541
- "Front-mount High-Flow":9.0
542
- }
543
- turbo_size_map = {
544
- "N/A":0.0,
545
- "Small 45-55mm":5.0,
546
- "Medium 56-65mm":12.0,
547
- "Big 66-75mm":20.0,
548
- "XL 76mm+":28.0
549
- }
550
 
551
- cam_gain = cam_gain_map.get(cam, 0.0)
552
- headers_gain = headers_gain_map.get(headers, 0.0)
553
- intake_man_gain = intake_man_gain_map.get(intake_manifold, 0.0)
554
- intercooler_gain = intercooler_gain_map.get(intercooler, 0.0)
555
- turbo_size_gain = turbo_size_map.get(turbo_size, 0.0)
556
-
557
- if induction in ["Turbo", "Twin-Turbo"]:
558
- boost_gain = boost_psi * 1.8
559
- elif induction in ["Supercharger", "Twincharged"]:
560
- boost_gain = boost_psi * 1.4
561
- else:
562
- boost_gain = 0.0
563
-
564
- meth_gain = 12.0 if meth else 0.0
565
- exhaust_dia_gain = max(0.0, (exhaust_dia - 55) * 0.1)
566
-
567
- extra_gain = (
568
- cam_gain +
569
- headers_gain +
570
- intake_man_gain +
571
- intercooler_gain +
572
- turbo_size_gain +
573
- boost_gain * 0.9 +
574
- meth_gain +
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:
593
- verdict = "Respectable street build 🌃"
594
- elif hype_level < 70:
595
- verdict = "Serious weekend weapon ⚔️"
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")
606
-
607
- m1, m2, m3 = st.columns(3)
608
- m1.metric("Estimated HP Gain", f"{pred_total_gain:.1f} HP")
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(
635
- {"Horsepower": [base_hp, new_hp]},
636
- index=["Stock", "Tuned"]
637
- )
638
- )
639
-
640
- st.markdown(
641
- f"""
642
- <div style="margin-top:6px;">
643
- <span class="chip">{engine_disp}L {engine_layout}</span>
644
- <span class="chip">Weight: {weight_kg} kg ▸ −{weight_reduction}%</span>
645
- <span class="chip">Intake: {intake}</span>
646
- <span class="chip">Headers: {headers}</span>
647
- <span class="chip">Exhaust: {exhaust} ({exhaust_dia}mm)</span><br/>
648
- <span class="chip">Induction: {induction} @ {boost_psi} psi</span>
649
- <span class="chip">IC: {intercooler}</span>
650
- <span class="chip">Tune: {tune}</span>
651
- <span class="chip">Fuel: {fuel}</span>
652
- </div>
653
- """,
654
- unsafe_allow_html=True
655
- )
656
-
657
- st.markdown("---")
658
-
659
- breakdown = pd.DataFrame({
660
- "Component": [
661
- "Model base (from dataset)",
662
- "Cam profile",
663
- "Headers",
664
- "Intake manifold",
665
- "Intercooler",
666
- "Turbo size",
667
- "Boost (net)",
668
- "Meth kit",
669
- "Exhaust diameter tweak"
670
- ],
671
- "Approx HP": [
672
- pred_base,
673
- cam_gain,
674
- headers_gain,
675
- intake_man_gain,
676
- intercooler_gain,
677
- turbo_size_gain,
678
- boost_gain * 0.9,
679
- meth_gain,
680
- exhaust_dia_gain
681
- ]
682
- })
683
-
684
- st.caption("🔬 Where is that extra power coming from?")
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
701
  )
 
 
1
  import streamlit as st
2
+
3
+ # -------------------------------------------------
4
+ # PAGE CONFIG
5
+ # -------------------------------------------------
 
 
 
 
6
  st.set_page_config(
7
+ page_title="Tune Your Build",
8
+ page_icon="🛠️",
9
+ layout="wide",
10
  )
11
 
12
+ # -------------------------------------------------
13
+ # GLOBAL CONSTANTS
14
+ # -------------------------------------------------
15
+ BASE_HP_MIN = 50
16
+ BASE_HP_MAX = 2000 # keep this high so no preset causes an error
17
+
18
+ ENGINE_LAYOUTS = ["I4", "I6", "V6", "V8", "V10", "V12"]
19
+
20
+ PRESETS = {
21
+ "daily": {
22
+ "label": "Daily Driver",
23
+ "emoji": "🚘",
24
+ "base_hp": 180,
25
+ "engine_l": 2.0,
26
+ "layout": "I4",
27
+ "turbo": False,
28
+ "intercooler": False,
29
+ "tune": False,
30
+ "intake": False,
31
+ "exhaust": True,
32
+ "fuel": "Petrol",
33
+ },
34
+ "street": {
35
+ "label": "Street Fun",
36
+ "emoji": "🏙️",
37
+ "base_hp": 320,
38
+ "engine_l": 3.0,
39
+ "layout": "V6",
40
+ "turbo": True,
41
+ "intercooler": False,
42
+ "tune": True,
43
+ "intake": True,
44
+ "exhaust": True,
45
+ "fuel": "Octane 97",
46
+ },
47
+ "drag": {
48
+ "label": "Drag Strip",
49
+ "emoji": "🏁",
50
+ "base_hp": 650,
51
+ "engine_l": 5.0,
52
+ "layout": "V8",
53
+ "turbo": True,
54
+ "intercooler": True,
55
+ "tune": True,
56
+ "intake": True,
57
+ "exhaust": True,
58
+ "fuel": "E85",
59
+ },
60
+ "max": {
61
+ "label": "Max Attack",
62
+ "emoji": "🔥",
63
+ "base_hp": 1100,
64
+ "engine_l": 6.0,
65
+ "layout": "V12",
66
+ "turbo": True,
67
+ "intercooler": True,
68
+ "tune": True,
69
+ "intake": True,
70
+ "exhaust": True,
71
+ "fuel": "E85",
72
+ },
73
+ }
74
+
75
+ # -------------------------------------------------
76
+ # CUSTOM CSS
77
+ # -------------------------------------------------
78
  st.markdown(
79
  """
80
+ <style>
81
+ /* Page background */
82
+ main.block-container {
83
+ padding-top: 1.5rem;
84
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
+ /* Top title bar */
87
+ .tune-header {
88
+ background: radial-gradient(circle at top left, #4c6fff55, #070b20 60%);
89
+ border-radius: 999px;
90
+ padding: 0.9rem 1.6rem;
91
+ border: 1px solid rgba(255,255,255,0.06);
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 0.8rem;
95
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
+ /* Preset buttons row */
98
+ .preset-row {
99
+ margin-top: 1.0rem;
100
+ margin-bottom: 1.0rem;
101
+ display: flex;
102
+ flex-wrap: wrap;
103
+ gap: 0.6rem;
104
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
+ /* Tabs container rounded card */
107
+ div[data-testid="stTabs"] {
108
+ background: rgba(5, 8, 26, 0.96);
109
+ padding: 1.15rem 1.15rem 0.85rem 1.15rem;
110
+ border-radius: 22px;
111
+ border: 1px solid rgba(255,255,255,0.06);
112
+ }
113
 
114
+ /* Tab buttons = pills */
115
+ div[data-testid="stTabs"] button[role="tab"] {
116
+ border-radius: 999px !important;
117
+ padding: 0.35rem 1.0rem;
118
+ border: 1px solid rgba(255,255,255,0.14);
119
+ background-color: rgba(255,255,255,0.02);
120
+ font-size: 0.85rem;
121
+ }
122
 
123
+ /* Active tab */
124
+ div[data-testid="stTabs"] button[role="tab"][aria-selected="true"] {
125
+ background: linear-gradient(135deg, #ff8a00cc, #ff3b6acc);
126
+ border-color: rgba(255,255,255,0.8);
127
+ }
128
 
129
+ /* Metric card tweaks */
130
+ [data-testid="stMetricValue"] {
131
+ font-size: 1.4rem;
132
+ }
133
+ </style>
134
+ """,
135
+ unsafe_allow_html=True,
136
+ )
137
 
138
+ # -------------------------------------------------
139
+ # PRESET HANDLING
140
+ # -------------------------------------------------
141
+ if "active_preset" not in st.session_state:
142
+ st.session_state.active_preset = "daily"
143
+
144
+
145
+ def apply_preset(name: str):
146
+ """Load preset values into session_state."""
147
+ p = PRESETS[name]
148
+ st.session_state.active_preset = name
149
+ st.session_state.engine_l = p["engine_l"]
150
+ st.session_state.engine_layout = p["layout"]
151
+ st.session_state.base_hp = min(max(p["base_hp"], BASE_HP_MIN), BASE_HP_MAX)
152
+ st.session_state.intake = p["intake"]
153
+ st.session_state.exhaust = p["exhaust"]
154
+ st.session_state.turbo = p["turbo"]
155
+ st.session_state.intercooler = p["intercooler"]
156
+ st.session_state.tune = p["tune"]
157
+ st.session_state.fuel = p["fuel"]
158
+
159
+
160
+ # initialise first time
161
+ if "base_hp" not in st.session_state:
162
+ apply_preset("daily")
163
+
164
+ # -------------------------------------------------
165
+ # HEADER
166
+ # -------------------------------------------------
167
+ st.markdown(
168
+ """
169
+ <div class="tune-header">
170
+ <span style="font-size:1.4rem;">🧪</span>
171
+ <div>
172
+ <div style="font-size:1.1rem; font-weight:600;">Tune Your Build</div>
173
+ <div style="font-size:0.85rem; opacity:0.8;">Estimate power gains from your mods in real-time.</div>
174
+ </div>
175
+ </div>
176
+ """,
177
+ unsafe_allow_html=True,
178
+ )
179
+
180
+ # -------------------------------------------------
181
+ # PRESET BUTTONS
182
+ # -------------------------------------------------
183
+ st.markdown('<div class="preset-row">', unsafe_allow_html=True)
184
+ cols = st.columns(len(PRESETS))
185
+
186
+ for i, (key, preset) in enumerate(PRESETS.items()):
187
+ with cols[i]:
188
+ if st.button(
189
+ f"{preset['emoji']} {preset['label']}",
190
+ key=f"preset_{key}",
191
+ use_container_width=True,
192
+ ):
193
+ apply_preset(key)
194
+ st.toast(f"Loaded {preset['label']} preset {preset['emoji']}")
195
+ st.markdown("</div>", unsafe_allow_html=True)
196
+
197
+ active_preset_name = PRESETS[st.session_state.active_preset]["label"]
198
+ st.caption(f"Active preset: **{active_preset_name}**")
199
+
200
+ # -------------------------------------------------
201
+ # TABS (CORE SPECS, BOLT-ONS, BOOST, ECU)
202
+ # -------------------------------------------------
203
+ tab1, tab2, tab3, tab4 = st.tabs(
204
+ ["⚙️ Core Specs", "🔩 Bolt-Ons", "💨 Boost & Cooling", "🧠 ECU & Fuel"]
205
+ )
206
 
207
+ # ---------------- CORE SPECS ----------------
208
+ with tab1:
209
+ c1, c2, c3 = st.columns([1, 1, 1])
210
 
211
+ with c1:
212
+ st.session_state.engine_l = st.number_input(
 
 
 
 
213
  "Engine Displacement (L)",
214
+ min_value=0.8,
215
+ max_value=8.0,
216
+ value=float(st.session_state.get("engine_l", 2.0)),
217
+ step=0.1,
 
 
 
 
 
218
  )
219
 
220
+ with c2:
221
+ st.session_state.engine_layout = st.selectbox(
222
+ "Engine Layout",
223
+ ENGINE_LAYOUTS,
224
+ index=ENGINE_LAYOUTS.index(
225
+ st.session_state.get("engine_layout", "I4")
226
+ ),
 
227
  )
228
 
229
+ with c3:
230
+ # clamp base hp to safe range
231
+ default_hp = int(st.session_state.get("base_hp", 180))
232
+ default_hp = min(max(default_hp, BASE_HP_MIN), BASE_HP_MAX)
233
+ st.session_state.base_hp = st.number_input(
234
+ "Base Horsepower (Stock Dyno)",
235
+ min_value=BASE_HP_MIN,
236
+ max_value=BASE_HP_MAX,
237
+ value=default_hp,
238
+ step=10,
239
  )
240
 
241
+ # ---------------- BOLT-ONS ----------------
242
+ with tab2:
243
+ c1, c2 = st.columns(2)
244
+ with c1:
245
+ st.session_state.intake = st.checkbox(
246
+ "High-Flow Intake", value=st.session_state.get("intake", False)
 
 
 
 
 
 
247
  )
248
+ st.session_state.exhaust = st.checkbox(
249
+ "Performance Exhaust", value=st.session_state.get("exhaust", False)
 
 
250
  )
251
+ with c2:
252
+ st.write("Typical Gains:")
253
+ st.write("• Intake: +10–20 hp")
254
+ st.write("• Exhaust: +20–40 hp")
255
+
256
+ # ---------------- BOOST & COOLING ----------------
257
+ with tab3:
258
+ c1, c2 = st.columns(2)
259
+ with c1:
260
+ st.session_state.turbo = st.checkbox(
261
+ "Turbo / Supercharger",
262
+ value=st.session_state.get("turbo", False),
 
263
  )
264
+ st.session_state.intercooler = st.checkbox(
265
+ "Upgraded Intercooler",
266
+ value=st.session_state.get("intercooler", False),
 
267
  )
268
+ with c2:
269
+ st.write("Boost Tips:")
270
+ st.write("• Forced induction gives the biggest jump in power.")
271
+ st.write("• Intercoolers help keep power consistent on long pulls.")
272
+
273
+ # ---------------- ECU & FUEL ----------------
274
+ with tab4:
275
+ c1, c2 = st.columns(2)
276
+ with c1:
277
+ st.session_state.tune = st.checkbox(
278
+ "ECU / Remap Tune",
279
+ value=st.session_state.get("tune", False),
280
  )
281
+ with c2:
282
+ st.session_state.fuel = st.selectbox(
283
  "Fuel Type",
284
+ ["Petrol", "Octane 97", "E85"],
285
+ index=["Petrol", "Octane 97", "E85"].index(
286
+ st.session_state.get("fuel", "Petrol")
287
+ ),
 
 
 
 
 
288
  )
289
 
290
+ # -------------------------------------------------
291
+ # PERFORMANCE CALCULATION
292
+ # -------------------------------------------------
293
+ hp = float(st.session_state.base_hp)
294
+
295
+ # bolt-ons
296
+ if st.session_state.intake:
297
+ hp += 15
298
+ if st.session_state.exhaust:
299
+ hp += 30
300
+
301
+ # boost
302
+ if st.session_state.turbo:
303
+ hp *= 1.40
304
+ if st.session_state.intercooler:
305
+ hp *= 1.08
306
+
307
+ # tune
308
+ if st.session_state.tune:
309
+ hp *= 1.12
310
+
311
+ # fuel
312
+ fuel = st.session_state.fuel
313
+ if fuel == "Octane 97":
314
+ hp *= 1.04
315
+ elif fuel == "E85":
316
+ hp *= 1.16
317
+
318
+ final_hp = int(hp)
319
+
320
+ # 0–100 km/h (simple heuristic)
321
+ zero_to_hundred = round(11.5 - (final_hp / 190), 2)
322
+ zero_to_hundred = max(zero_to_hundred, 2.2)
323
+
324
+ # top speed estimate
325
+ top_speed = int(170 + final_hp / 4.2)
326
+
327
+ # -------------------------------------------------
328
+ # RESULTS
329
+ # -------------------------------------------------
330
+ st.markdown("---")
331
+ st.subheader("📊 Estimated Performance")
332
+
333
+ mc1, mc2, mc3 = st.columns(3)
334
+
335
+ with mc1:
336
+ st.metric(
337
+ "Final Horsepower",
338
+ f"{final_hp} hp",
339
+ f"+{final_hp - int(st.session_state.base_hp)} hp vs stock",
340
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
 
342
+ with mc2:
343
+ st.metric("0–100 km/h", f"{zero_to_hundred} s")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
 
345
+ with mc3:
346
+ st.metric("Estimated Top Speed", f"{top_speed} km/h")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
348
+ # Small hint
349
+ st.info(
350
+ "💡 Hint: For big gains, combine a turbo/supercharger with a good intercooler and a proper ECU tune."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  )
352
+