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

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +183 -198
src/streamlit_app.py CHANGED
@@ -4,115 +4,119 @@ import numpy as np
4
  from sklearn.preprocessing import StandardScaler
5
  from sklearn.neighbors import KNeighborsRegressor
6
 
7
- # ---------------------------
8
- # 1. Configuration & Custom CSS (MacOS Theme)
9
- # ---------------------------
10
- st.set_page_config(
11
- page_title="DynoPro for Mac",
12
- page_icon="πŸ–₯️",
13
- layout="wide",
14
- initial_sidebar_state="expanded"
15
- )
16
 
17
- # Custom CSS for the Apple/MacOS Aesthetic
18
  st.markdown("""
19
- <style>
20
- /* Main Background - Apple Light Gray */
 
 
 
 
 
21
  .stApp {
22
- background-color: #F5F5F7;
23
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
24
  }
25
 
26
- /* Sidebar - White with border */
27
  section[data-testid="stSidebar"] {
28
- background-color: #FFFFFF;
29
- border-right: 1px solid #E5E5E5;
30
  }
31
 
32
- /* Headings */
33
- h1, h2, h3 {
34
- color: #1D1D1F;
35
- font-weight: 600;
36
  }
37
 
38
- /* Metrics - Card Style */
39
- div[data-testid="stMetric"] {
40
- background-color: #FFFFFF;
41
- padding: 15px;
 
 
 
 
 
 
 
 
 
42
  border-radius: 12px;
43
- box-shadow: 0 2px 8px rgba(0,0,0,0.04);
44
- border: 1px solid #EAEAEA;
45
  }
46
 
47
- /* Custom Container Card */
48
- .mac-card {
49
- background-color: #FFFFFF;
50
- padding: 20px;
51
- border-radius: 16px;
52
- box-shadow: 0 4px 12px rgba(0,0,0,0.05);
53
- margin-bottom: 20px;
 
 
 
 
 
54
  }
55
 
56
- /* Button Styling */
57
- .stButton>button {
58
- border-radius: 8px;
59
- background-color: #0071E3;
60
- color: white;
61
- border: none;
62
  }
63
- </style>
64
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
65
 
66
- # ---------------------------
67
- # 2. Advanced Dataset Generation
68
- # ---------------------------
69
  @st.cache_resource
70
- def build_engine_model(n=800):
71
  np.random.seed(42)
 
72
  data = []
73
 
74
  for _ in range(n):
75
- # Expanded Engine Options
76
- cyl = np.random.choice([3, 4, 5, 6, 8, 10, 12])
 
77
 
78
- # Displacement logic based on cyl
79
- if cyl <= 4: engine = np.random.uniform(1.0, 2.5)
80
- elif cyl <= 6: engine = np.random.uniform(2.5, 4.0)
81
- elif cyl <= 8: engine = np.random.uniform(4.0, 6.2)
82
- else: engine = np.random.uniform(5.0, 8.4) # V10/V12
 
 
83
 
84
- # Base HP Calculation (more realistic variance)
85
- specific_output = np.random.uniform(60, 110) # HP per Liter
86
- base_hp = int(engine * specific_output)
87
-
88
- # Mods (0=Stock)
89
- intake = np.random.choice([0, 1, 2]) # Stock/CAI/Race
90
- headers = np.random.choice([0, 1]) # Stock/Aftermarket
91
- exhaust = np.random.choice([0, 1, 2]) # Stock/Sport/Straight
92
- induction = np.random.choice([0, 1, 2, 3]) # None/Turbo/Twin-Turbo/Super
93
- intercooler = np.random.choice([0, 1]) # Stock/Upgraded
94
- fuel = np.random.choice([0, 1, 2, 3]) # 87/91/93/E85
95
- tune = np.random.choice([0, 1, 2, 3]) # None/Stage1/Stage2/Custom
96
-
97
- # Logic: V12s gain more from exhaust/headers, Turbos gain huge from intercoolers
98
- hp_gain = (
99
- (intake * 4) +
100
- (headers * (cyl * 1.5)) +
101
- (exhaust * 8) +
102
- (induction * (base_hp * 0.30)) + # % gain based on base HP
103
- (intercooler * (15 if induction > 0 else 0)) +
104
- (tune * 15) +
105
- (fuel * 5) +
106
  np.random.uniform(-5, 5)
107
  )
108
 
109
- # Diminishing returns for N/A engines
110
- if induction == 0:
111
- hp_gain = hp_gain * 0.7
112
 
113
- data.append([engine, cyl, base_hp, intake, headers, exhaust, induction, intercooler, fuel, tune, hp_gain])
114
 
115
- columns = ["engine", "cyl", "base_hp", "intake", "headers", "exhaust", "induction", "intercooler", "fuel", "tune", "hp_gain"]
116
  df = pd.DataFrame(data, columns=columns)
117
 
118
  X = df.drop("hp_gain", axis=1)
@@ -126,140 +130,121 @@ def build_engine_model(n=800):
126
 
127
  return scaler, model
128
 
129
- scaler, model = build_engine_model()
 
 
 
 
130
 
131
- # ---------------------------
132
- # 3. Sidebar (The "Control Center")
133
- # ---------------------------
134
  with st.sidebar:
135
- st.title("πŸŽ›οΈ Tuner Studio")
136
- st.caption("Vehicle Configuration")
137
-
138
- st.subheader("Base Vehicle")
139
- cyl = st.select_slider("Cylinders", options=[3, 4, 5, 6, 8, 10, 12], value=6)
140
- engine = st.number_input("Displacement (L)", 1.0, 8.4, 3.0, step=0.1)
141
- base_hp = st.number_input("Factory Horsepower", 80, 1200, 300, step=10)
142
-
143
  st.markdown("---")
144
- st.subheader("Modifications")
145
 
146
- # Air & Exhaust
147
- with st.expander("πŸ’¨ Air & Exhaust", expanded=True):
148
- intake = st.selectbox("Intake", ["Stock", "Cold Air", "Race Intake"])
149
- headers = st.toggle("Aftermarket Headers")
150
- exhaust = st.select_slider("Exhaust System", options=["Stock", "Sport Cat-back", "Straight Pipe"])
151
-
152
- # Engine & Power Adders
153
- with st.expander("⚑ Induction & Tune", expanded=True):
 
 
 
 
 
 
154
  induction = st.selectbox("Forced Induction", ["Naturally Aspirated", "Single Turbo", "Twin Turbo", "Supercharger"])
155
- intercooler = st.toggle("Upgraded Intercooler")
156
- tune = st.select_slider("ECU Tune", options=["Stock Map", "Stage 1", "Stage 2", "Custom Dyno"])
157
- fuel = st.selectbox("Fuel Type", ["87 Octane", "91 Octane", "93 Octane", "E85 / Race Gas"])
158
 
159
- # ---------------------------
160
- # 4. Processing
161
- # ---------------------------
162
- # Mappings
163
- map_intake = {"Stock":0, "Cold Air":1, "Race Intake":2}
164
- map_exhaust = {"Stock":0, "Sport Cat-back":1, "Straight Pipe":2}
165
- map_induct = {"Naturally Aspirated":0, "Single Turbo":1, "Twin Turbo":2, "Supercharger":3}
166
- map_tune = {"Stock Map":0, "Stage 1":1, "Stage 2":2, "Custom Dyno":3}
167
- map_fuel = {"87 Octane":0, "91 Octane":1, "93 Octane":2, "E85 / Race Gas":3}
168
 
169
- input_vector = np.array([[
 
 
 
 
 
 
 
 
 
 
170
  engine, cyl, base_hp,
171
- map_intake[intake],
172
- 1 if headers else 0,
173
- map_exhaust[exhaust],
174
- map_induct[induction],
175
- 1 if intercooler else 0,
176
- map_fuel[fuel],
177
- map_tune[tune]
178
  ]])
179
 
180
- input_scaled = scaler.transform(input_vector)
181
- pred_gain = model.predict(input_scaled)[0]
182
- final_hp = base_hp + pred_gain
183
-
184
- # ---------------------------
185
- # 5. Main Dashboard (MacOS Style)
186
- # ---------------------------
187
 
188
- # Header
189
- st.markdown("### 🏎️ Dyno Simulation Results")
190
- st.markdown("This dashboard estimates power output based on component synergy.")
191
- st.write("") # Spacer
 
 
 
 
 
 
 
 
192
 
193
- # Top Row: The "Hero" Cards
194
- col1, col2 = st.columns([1, 2])
195
 
196
  with col1:
197
- # Summary Card
198
- with st.container(border=True):
199
- st.markdown("**Vehicle Profile**")
200
- st.markdown(f"<h1 style='margin:0; font-size: 40px;'>{str(cyl)}<span style='font-size:20px; color:gray'>cyl</span></h1>", unsafe_allow_html=True)
201
- st.caption(f"{engine}L Displacement")
202
- st.divider()
203
- st.write(f"**Induction:** {induction}")
204
- st.write(f"**Tune:** {tune}")
 
 
 
 
 
 
205
 
206
  with col2:
207
- # Results Card
208
- with st.container(border=True):
209
- st.markdown("**Projected Output**")
 
 
 
 
 
 
 
210
 
211
- m1, m2, m3 = st.columns(3)
212
- with m1:
213
- st.metric("Base Power", f"{base_hp} HP")
214
- with m2:
215
- st.metric("Gain", f"+{pred_gain:.0f} HP", delta=f"{((pred_gain/base_hp)*100):.1f}%")
216
- with m3:
217
- st.metric("Total Power", f"{final_hp:.0f} HP", delta="Peak Output")
218
 
219
- # Progress Bar visual
220
- st.write("")
221
- st.write("Power Utilization")
222
- pct_gain = min((final_hp / (base_hp * 2)), 1.0) # Cap bar at 200% base
223
- st.progress(pct_gain)
224
-
225
- # Bottom Row: Visualization
226
- st.write("")
227
- st.markdown("### πŸ“ˆ Power Curve Analysis")
228
-
229
- with st.container(border=True):
230
- # Creating a synthetic RPM curve for visualization
231
- rpms = np.linspace(2000, 8000, 50)
232
-
233
- # Physics approximation for torque curve shape
234
- def torque_curve(rpm, peak_hp):
235
- peak_rpm = 6500
236
- # simplified curve logic
237
- return -((rpm - peak_rpm)**2) + (peak_hp * 5000)
238
-
239
- # Normalized curves scaled to HP
240
- base_curve = [base_hp * (1 - ((x - 6500)/5000)**2) * (x/8000) for x in rpms]
241
- mod_curve = [final_hp * (1 - ((x - 6500)/5000)**2) * (x/8000) for x in rpms]
242
-
243
- chart_data = pd.DataFrame({
244
- "RPM": np.tile(rpms, 2),
245
- "Horsepower": np.concatenate([base_curve, mod_curve]),
246
- "Setup": ["Factory Stock"] * 50 + ["Modified"] * 50
247
- })
248
-
249
- # Use Streamlit's native area chart but configure it to look clean
250
- st.area_chart(
251
- chart_data,
252
- x="RPM",
253
- y="Horsepower",
254
- color="Setup",
255
- stack=False # Overlay them
256
- )
257
-
258
- # Footer / "Dock" feel
259
- st.divider()
260
- cols = st.columns(4)
261
- cols[0].info(f"Fuel: {fuel}")
262
- cols[1].info(f"Exhaust: {exhaust}")
263
- cols[2].info(f"Headers: {'Yes' if headers else 'No'}")
264
- cols[3].info(f"Intercooler: {'Yes' if intercooler else 'No'}")
265
-
 
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)
 
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="⚠️")