NeuralGearheads commited on
Commit
e0b1f9c
ยท
verified ยท
1 Parent(s): 8d043f9

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +667 -317
src/streamlit_app.py CHANGED
@@ -1,352 +1,702 @@
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
 
 
 
 
 
 
 
 
 
 
 
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
+ # FIX: Increased max_value to 3000 to prevent crash on high displacement/cylinder combinations
419
+ base_hp = st.number_input(
420
+ "Base Horsepower (stock dyno)",
421
+ min_value=60, max_value=3000,
422
+ value=int(max(90, round(engine_disp * cyl * 20))),
423
+ key="base_hp"
 
424
  )
425
 
426
+ col3, col4 = st.columns(2)
427
+ weight_kg = col3.number_input(
428
+ "Vehicle Weight (kg)",
429
+ min_value=700, max_value=4000, value=1500,
430
+ key="weight_kg"
 
431
  )
432
+ weight_reduction = col4.slider(
433
+ "Weight Reduction (%)",
434
+ 0, 40, 0,
435
+ key="weight_reduction"
436
  )
437
+
438
+ # --- Bolt-ons ---
439
+ with tabs[1]:
440
+ col1, col2, col3 = st.columns(3)
441
+ intake = col1.selectbox("Intake", ["Stock", "Cold Air", "Performance"], key="intake")
442
+ headers = col2.selectbox("Headers", ["Stock", "Shorty", "Long Tube"], key="headers")
443
+ exhaust = col3.selectbox("Exhaust", ["Stock", "Cat-back", "Straight Pipe"], key="exhaust")
444
+ exhaust_dia = st.slider("Exhaust Diameter (mm)", 40, 120, 60, key="exhaust_dia")
445
+
446
+ cam = st.selectbox(
447
+ "Cam Profile",
448
+ ["Stock", "Stage 1 โ€“ Road", "Stage 2 โ€“ Aggressive", "Stage 3 โ€“ Race"],
449
+ key="cam"
450
+ )
451
+ intake_manifold = st.selectbox(
452
+ "Intake Manifold",
453
+ ["Stock", "High-flow", "Individual throttle bodies"],
454
+ key="intake_manifold"
455
+ )
456
+
457
+ # --- Boost & cooling ---
458
+ with tabs[2]:
459
+ induction = st.selectbox(
460
+ "Forced Induction Setup",
461
+ ["None", "Turbo", "Twin-Turbo", "Supercharger", "Twincharged"],
462
+ key="induction"
463
+ )
464
+ boost_psi = st.slider("Target Boost (psi)", 0, 40, 10, key="boost_psi")
465
+ turbo_size = st.selectbox(
466
+ "Turbo Size",
467
+ ["N/A", "Small 45-55mm", "Medium 56-65mm", "Big 66-75mm", "XL 76mm+"],
468
+ key="turbo_size"
469
  )
470
+ intercooler = st.selectbox(
471
+ "Intercooler Type",
472
+ ["None", "Air-to-Air", "Air-to-Water", "Front-mount High-Flow"],
473
+ key="intercooler"
474
  )
475
+ meth = st.checkbox("Methanol Injection Kit", value=False, key="meth")
476
+
477
+ # --- ECU & fuel ---
478
+ with tabs[3]:
479
+ tune = st.selectbox(
480
+ "ECU Tune Level",
481
+ ["None", "Mild Street", "Stage 1", "Stage 2", "Kill Mode"],
482
+ key="tune"
 
 
 
 
483
  )
484
+ fuel = st.selectbox(
 
485
  "Fuel Type",
486
+ ["87", "91", "93", "E85 / Race Blend"],
487
+ key="fuel"
488
+ )
489
+ altitude = st.slider("Altitude (meters)", 0, 3500, 200, key="altitude")
490
+ traction_mode = st.radio(
491
+ "Traction Mode Vibe",
492
+ ["Daily", "Spirited", "Track / Drag"],
493
+ horizontal=True,
494
+ key="traction_mode"
495
  )
496
 
497
+ st.markdown(
498
+ '<p class="muted" style="margin-top:8px;">Numbers are synthetic; this is a dyno-inspired playground, not a tuning bible. ๐Ÿงช</p>',
499
+ unsafe_allow_html=True
500
+ )
501
+ st.markdown('</div>', unsafe_allow_html=True)
502
+
503
+ # ========= MODEL & CALCS =========
504
+
505
+ intake_map = {"Stock":0, "Cold Air":1, "Performance":2}
506
+ exhaust_map = {"Stock":0, "Cat-back":1, "Straight Pipe":2}
507
+ induction_model_map = {"None":0, "Turbo":1, "Twin-Turbo":1, "Supercharger":2, "Twincharged":2}
508
+ fuel_map = {"87":0, "91":1, "93":2, "E85 / Race Blend":3}
509
+ tune_base_map = {"None":0, "Mild Street":1, "Stage 1":1, "Stage 2":2, "Kill Mode":2}
510
+
511
+ input_for_model = np.array([[
512
+ engine_disp,
513
+ cyl,
514
+ base_hp,
515
+ intake_map.get(intake,0),
516
+ exhaust_map.get(exhaust,0),
517
+ induction_model_map.get(induction,0),
518
+ fuel_map.get(fuel,0),
519
+ tune_base_map.get(tune,0),
520
+ altitude
521
+ ]])
522
+
523
+ input_scaled = scaler.transform(input_for_model)
524
+ pred_base = float(model.predict(input_scaled)[0])
525
+
526
+ cam_gain_map = {
527
+ "Stock":0.0,
528
+ "Stage 1 โ€“ Road":6.0,
529
+ "Stage 2 โ€“ Aggressive":12.0,
530
+ "Stage 3 โ€“ Race":20.0
531
+ }
532
+ headers_gain_map = {"Stock":0.0, "Shorty":5.0, "Long Tube":9.0}
533
+ intake_man_gain_map = {
534
+ "Stock":0.0,
535
+ "High-flow":5.0,
536
+ "Individual throttle bodies":10.0
537
+ }
538
+ intercooler_gain_map = {
539
+ "None":0.0,
540
+ "Air-to-Air":4.0,
541
+ "Air-to-Water":6.5,
542
+ "Front-mount High-Flow":9.0
543
+ }
544
+ turbo_size_map = {
545
+ "N/A":0.0,
546
+ "Small 45-55mm":5.0,
547
+ "Medium 56-65mm":12.0,
548
+ "Big 66-75mm":20.0,
549
+ "XL 76mm+":28.0
550
+ }
551
+
552
+ cam_gain = cam_gain_map.get(cam, 0.0)
553
+ headers_gain = headers_gain_map.get(headers, 0.0)
554
+ intake_man_gain = intake_man_gain_map.get(intake_manifold, 0.0)
555
+ intercooler_gain = intercooler_gain_map.get(intercooler, 0.0)
556
+ turbo_size_gain = turbo_size_map.get(turbo_size, 0.0)
557
+
558
+ if induction in ["Turbo", "Twin-Turbo"]:
559
+ boost_gain = boost_psi * 1.8
560
+ elif induction in ["Supercharger", "Twincharged"]:
561
+ boost_gain = boost_psi * 1.4
562
+ else:
563
+ boost_gain = 0.0
564
+
565
+ meth_gain = 12.0 if meth else 0.0
566
+ exhaust_dia_gain = max(0.0, (exhaust_dia - 55) * 0.1)
567
+
568
+ extra_gain = (
569
+ cam_gain +
570
+ headers_gain +
571
+ intake_man_gain +
572
+ intercooler_gain +
573
+ turbo_size_gain +
574
+ boost_gain * 0.9 +
575
+ meth_gain +
576
+ exhaust_dia_gain
577
+ )
578
+
579
+ traction_multiplier = {"Daily":0.9, "Spirited":1.0, "Track / Drag":1.05}[traction_mode]
580
+
581
+ pred_total_gain = (pred_base + extra_gain) * traction_multiplier
582
+ pred_total_gain = max(pred_total_gain, -5.0)
583
+ new_hp = max(base_hp + pred_total_gain, 40.0)
584
+
585
+ effective_weight = weight_kg * (1 - weight_reduction / 100.0)
586
+ hp_per_ton = new_hp / (effective_weight / 1000.0)
587
+
588
+ hype_raw = np.clip(pred_total_gain / 120 * 100, 0, 100)
589
+ hype_level = int(hype_raw)
590
+
591
+ if hype_level < 20:
592
+ verdict = "Sleeper grocery getter ๐Ÿš™"
593
+ elif hype_level < 40:
594
+ verdict = "Respectable street build ๐ŸŒƒ"
595
+ elif hype_level < 70:
596
+ verdict = "Serious weekend weapon โš”๏ธ"
597
+ else:
598
+ verdict = "Full send, tyres cry for mercy ๐Ÿ"
599
+
600
+ # compute base angle for rev gauge (-90deg to +90deg)
601
+ angle_deg = -90 + (hype_level / 100.0) * 180
602
+
603
+ # ========= RIGHT SIDE =========
604
+ with right:
605
+ st.markdown('<div class="glass">', unsafe_allow_html=True)
606
+ st.subheader("๐Ÿ’ฅ Build Outcome")
607
+
608
+ m1, m2, m3 = st.columns(3)
609
+ m1.metric("Estimated HP Gain", f"{pred_total_gain:.1f} HP")
610
+ m2.metric("New Output", f"{new_hp:.1f} HP")
611
+ m3.metric("HP per Ton", f"{hp_per_ton:.1f}")
612
+
613
+ # Animated rev limiter gauge
614
+ st.markdown('<div class="meter-label">Rev Limiter Vibes</div>', unsafe_allow_html=True)
615
+ st.markdown(
616
+ f"""
617
+ <div class="rev-wrap">
618
+ <div class="rev-gauge">
619
+ <div class="rev-arc"></div>
620
+ <div class="rev-needle" style="--base-angle:{angle_deg}deg;"></div>
621
+ <div class="rev-center">RPM</div>
622
+ <div class="rev-label">REDLINE</div>
623
+ </div>
624
+ </div>
625
+ """,
626
+ unsafe_allow_html=True
627
  )
628
 
629
+ st.markdown('<div class="meter-label">Hype Meter (relative craziness of this build)</div>', unsafe_allow_html=True)
630
+ st.progress(hype_level)
631
+ st.markdown(f"**Verdict:** {verdict}")
632
 
633
+ st.markdown("")
634
+ st.bar_chart(
635
+ pd.DataFrame(
636
+ {"Horsepower": [base_hp, new_hp]},
637
+ index=["Stock", "Tuned"]
638
+ )
639
+ )
640
 
641
+ st.markdown(
642
+ f"""
643
+ <div style="margin-top:6px;">
644
+ <span class="chip">{engine_disp}L {engine_layout}</span>
645
+ <span class="chip">Weight: {weight_kg} kg โ–ธ โˆ’{weight_reduction}%</span>
646
+ <span class="chip">Intake: {intake}</span>
647
+ <span class="chip">Headers: {headers}</span>
648
+ <span class="chip">Exhaust: {exhaust} ({exhaust_dia}mm)</span><br/>
649
+ <span class="chip">Induction: {induction} @ {boost_psi} psi</span>
650
+ <span class="chip">IC: {intercooler}</span>
651
+ <span class="chip">Tune: {tune}</span>
652
+ <span class="chip">Fuel: {fuel}</span>
653
+ </div>
654
+ """,
655
+ unsafe_allow_html=True
656
+ )
657
+
658
+ st.markdown("---")
659
+
660
+ breakdown = pd.DataFrame({
661
+ "Component": [
662
+ "Model base (from dataset)",
663
+ "Cam profile",
664
+ "Headers",
665
+ "Intake manifold",
666
+ "Intercooler",
667
+ "Turbo size",
668
+ "Boost (net)",
669
+ "Meth kit",
670
+ "Exhaust diameter tweak"
671
+ ],
672
+ "Approx HP": [
673
+ pred_base,
674
+ cam_gain,
675
+ headers_gain,
676
+ intake_man_gain,
677
+ intercooler_gain,
678
+ turbo_size_gain,
679
+ boost_gain * 0.9,
680
+ meth_gain,
681
+ exhaust_dia_gain
682
+ ]
683
+ })
684
+
685
+ st.caption("๐Ÿ”ฌ Where is that extra power coming from?")
686
+ st.dataframe(breakdown, use_container_width=True, height=260)
687
+
688
+ st.markdown(
689
+ '<p class="muted" style="margin-top:8px;">Model: distance-weighted KNN on synthetic dyno-style data + heuristic math for advanced mods.</p>',
690
+ unsafe_allow_html=True
691
+ )
692
+ st.markdown('</div>', unsafe_allow_html=True)
693
 
694
+ # ========= FOOTER =========
695
+ st.markdown(
696
+ """
697
+ <div style="text-align:center;margin-top:10px;" class="muted">
698
+ Presets + rev limiter + neon dynoโ€ฆ if this doesnโ€™t make people go mad, we can push it even further. ๐Ÿ”ง๐Ÿ”ฅ
699
+ </div>
700
+ """,
701
+ unsafe_allow_html=True
702
+ )