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

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +195 -183
src/streamlit_app.py CHANGED
@@ -4,115 +4,115 @@ import numpy as np
4
  from sklearn.preprocessing import StandardScaler
5
  from sklearn.neighbors import KNeighborsRegressor
6
 
7
- # ---------------------------------------------------------
8
- # 1. macOS STYLING (CSS Injection)
9
- # ---------------------------------------------------------
10
- st.set_page_config(page_title="Mac-Mod Tuner", page_icon="πŸ–₯️", layout="wide")
 
 
 
 
 
11
 
 
12
  st.markdown("""
13
- <style>
14
- /* Main Background - Apple Light Grey */
15
  .stApp {
16
- background-color: #f5f5f7;
17
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
18
  }
19
 
20
- /* Sidebar Styling */
21
  section[data-testid="stSidebar"] {
22
- background-color: #e8e8ed; /* Slightly darker grey */
23
- border-right: 1px solid #d1d1d6;
24
  }
25
 
26
- /* Card/Container Styling (The "Window" look) */
27
- div[data-testid="stVerticalBlock"] > div[style*="background-color"] {
28
- background-color: white;
29
- border-radius: 18px;
30
- padding: 20px;
31
- box-shadow: 0 4px 20px rgba(0,0,0,0.05);
32
- border: 1px solid #e5e5ea;
33
- }
34
-
35
- /* Input Widgets - Rounded pills */
36
- .stSelectbox > div > div {
37
- border-radius: 12px;
38
- border: 1px solid #d1d1d6;
39
- }
40
- .stNumberInput > div > div > input {
41
- border-radius: 12px;
42
- }
43
-
44
- /* Metrics Styling */
45
- div[data-testid="stMetricValue"] {
46
  font-weight: 600;
47
- color: #1d1d1f;
48
  }
49
 
50
- /* Headers */
51
- h1, h2, h3 {
52
- color: #1d1d1f;
53
- font-weight: 700;
54
- letter-spacing: -0.5px;
 
 
55
  }
56
 
57
- /* Custom "Traffic Lights" for visual flair */
58
- .traffic-lights {
59
- display: flex;
60
- gap: 8px;
 
 
61
  margin-bottom: 20px;
62
  }
63
- .dot { width: 12px; height: 12px; border-radius: 50%; }
64
- .red { background-color: #ff5f57; border: 1px solid #e0443e; }
65
- .yellow { background-color: #febc2e; border: 1px solid #d89e24; }
66
- .green { background-color: #28c840; border: 1px solid #1aab29; }
67
- </style>
68
- """, unsafe_allow_html=True)
69
-
70
- # ---------------------------------------------------------
71
- # 2. DATA & MODEL (Expanded for V10/V12 & Nitrous)
72
- # ---------------------------------------------------------
73
 
 
 
 
74
  @st.cache_resource
75
- def train_model():
76
  np.random.seed(42)
77
- n = 1000 # More data for better accuracy with V12s
78
  data = []
79
 
80
  for _ in range(n):
81
  # Expanded Engine Options
82
- engine = np.random.choice([1.6, 2.0, 2.4, 3.0, 3.8, 4.0, 5.0, 5.2, 6.0, 6.5])
83
- cyl = np.random.choice([4, 6, 8, 10, 12])
84
 
85
- # Base HP logic (roughly 80-120 hp per liter depending on tech)
86
- base_hp = int(engine * np.random.uniform(60, 110))
 
 
 
87
 
88
- # Mods
89
- intake = np.random.choice([0, 1, 2]) # Stock/Sport/Race
90
- exhaust = np.random.choice([0, 1, 2, 3]) # Stock/Catback/Headers/Straight
 
 
 
 
 
91
  induction = np.random.choice([0, 1, 2, 3]) # None/Turbo/Twin-Turbo/Super
92
- cams = np.random.choice([0, 1, 2]) # Stock/Street/Track
93
- nitrous = np.random.choice([0, 1]) # No/Yes (50 shot equivalent)
94
  fuel = np.random.choice([0, 1, 2, 3]) # 87/91/93/E85
95
- tune = np.random.choice([0, 1, 2]) # None/Stage 1/Stage 2
96
 
97
- # Gain Logic
98
- gain = (
99
- (intake * 3) +
100
- (exhaust * 5) +
101
- (induction * (base_hp * 0.35)) + # Forced induction is % based
102
- (cams * (base_hp * 0.10)) +
103
- (nitrous * 50) +
104
- (tune * (base_hp * 0.08)) +
105
- (fuel * 4) +
106
  np.random.uniform(-5, 5)
107
  )
108
 
109
- # Diminishing returns for small engines with big mods
110
- if cyl < 6 and gain > 200:
111
- gain *= 0.8
112
 
113
- data.append([engine, cyl, base_hp, intake, exhaust, induction, cams, nitrous, fuel, tune, gain])
114
 
115
- columns = ["engine", "cyl", "base_hp", "intake", "exhaust", "induction", "cams", "nitrous", "fuel", "tune", "hp_gain"]
116
  df = pd.DataFrame(data, columns=columns)
117
 
118
  X = df.drop("hp_gain", axis=1)
@@ -126,128 +126,140 @@ def train_model():
126
 
127
  return scaler, model
128
 
129
- scaler, model = train_model()
130
-
131
- # ---------------------------------------------------------
132
- # 3. SIDEBAR CONTROLS (The "Settings Pane")
133
- # ---------------------------------------------------------
134
 
 
 
 
135
  with st.sidebar:
136
- st.header("βš™οΈ Configuration")
 
 
 
 
 
 
 
 
 
137
 
138
- with st.expander("πŸš™ Base Vehicle Stats", expanded=True):
139
- col_eng_1, col_eng_2 = st.columns(2)
140
- with col_eng_1:
141
- cyl = st.selectbox("Cylinders", [4, 5, 6, 8, 10, 12])
142
- with col_eng_2:
143
- 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])
144
-
145
- base_hp = st.number_input("Factory HP", 100, 1000, 300)
146
-
147
- with st.expander("πŸ”§ Bolt-on Modifications", expanded=True):
148
- intake = st.selectbox("Intake", ["Stock", "High Flow Filter", "Cold Air Intake"])
149
- exhaust = st.selectbox("Exhaust", ["Stock", "Cat-back", "Long Tube Headers", "Full Straight Pipe"])
150
-
151
- with st.expander("πŸ”₯ Internals & Boost", expanded=True):
152
  induction = st.selectbox("Forced Induction", ["Naturally Aspirated", "Single Turbo", "Twin Turbo", "Supercharger"])
153
- cams = st.selectbox("Camshafts", ["Stock", "Street Profile", "Track/Race Profile"])
154
- nitrous = st.checkbox("Nitrous Oxide System (NOS)", value=False)
155
-
156
- with st.expander("πŸ’» Tuning & Fuel", expanded=True):
157
- fuel = st.selectbox("Fuel Type", ["87 Octane", "91 Octane", "93 Octane", "E85 (Ethanol)"])
158
- tune = st.selectbox("ECU Map", ["Stock Map", "Stage 1", "Stage 2"])
159
-
160
- # ---------------------------------------------------------
161
- # 4. MAIN DASHBOARD (The "App View")
162
- # ---------------------------------------------------------
163
-
164
- # Prediction Mappings
165
- intake_map = {"Stock":0, "High Flow Filter":1, "Cold Air Intake":2}
166
- exhaust_map = {"Stock":0, "Cat-back":1, "Long Tube Headers":2, "Full Straight Pipe":3}
167
- induction_map = {"Naturally Aspirated":0, "Single Turbo":1, "Twin Turbo":2, "Supercharger":3}
168
- cams_map = {"Stock":0, "Street Profile":1, "Track/Race Profile":2}
169
- nitrous_map = {False:0, True:1}
170
- fuel_map = {"87 Octane":0, "91 Octane":1, "93 Octane":2, "E85 (Ethanol)":3}
171
- tune_map = {"Stock Map":0, "Stage 1":1, "Stage 2":2}
172
-
173
- # Calculation
174
- input_data = np.array([[
175
- engine, cyl, base_hp,
176
- intake_map[intake], exhaust_map[exhaust], induction_map[induction],
177
- cams_map[cams], nitrous_map[nitrous], fuel_map[fuel], tune_map[tune]
178
- ]])
179
 
180
- pred = model.predict(scaler.transform(input_data))[0]
181
- new_hp = base_hp + pred
182
- pct_gain = (pred / base_hp) * 100
 
 
 
 
 
 
183
 
184
- # --- UI LAYOUT ---
 
 
 
 
 
 
 
 
 
185
 
186
- # 1. Header with Fake Window Controls
187
- st.markdown("""
188
- <div class="traffic-lights">
189
- <div class="dot red"></div>
190
- <div class="dot yellow"></div>
191
- <div class="dot green"></div>
192
- </div>
193
- """, unsafe_allow_html=True)
194
 
195
- st.title("Performance Estimator Pro")
196
- st.markdown(f"Analysis for **{cyl}-Cylinder {engine}L Engine**")
 
197
 
198
- st.divider()
 
 
 
199
 
200
- # 2. Main Hero Section (Glass Cards)
201
- col1, col2 = st.columns([1.5, 1])
202
 
203
  with col1:
204
- st.markdown("### πŸ“ˆ Dyno Projection")
205
- # Area chart looks cleaner/more modern than bar
206
- chart_data = pd.DataFrame({
207
- "Horsepower": [base_hp, new_hp],
208
- "Stage": ["Factory", "Modified"]
209
- })
210
-
211
- # Custom color for the chart to match Apple blue
212
- st.vega_lite_chart(chart_data, {
213
- "mark": {"type": "bar", "cornerRadiusEnd": 4, "color": "#007AFF"},
214
- "encoding": {
215
- "x": {"field": "Stage", "type": "nominal", "axis": {"labelAngle": 0}},
216
- "y": {"field": "Horsepower", "type": "quantitative"},
217
- "tooltip": ["Stage", "Horsepower"]
218
- }
219
- }, use_container_width=True)
220
 
221
  with col2:
222
- st.markdown("### ⚑ Results")
223
-
224
- # Container for the metrics
225
- with st.container():
226
- st.markdown(f"""
227
- <div style="padding: 10px;">
228
- <span style="font-size: 14px; color: #86868b; text-transform: uppercase; letter-spacing: 1px; font-weight: 600;">Total Power</span>
229
- <div style="font-size: 48px; font-weight: 700; color: #1d1d1f; line-height: 1.2;">{int(new_hp)} HP</div>
230
- </div>
231
- """, unsafe_allow_html=True)
232
 
233
- st.divider()
 
 
 
 
 
 
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
- # 3. Spec Summary Row
242
- st.markdown("### πŸ“‹ Build Summary")
243
- with st.container():
244
- c1, c2, c3, c4 = st.columns(4)
245
- c1.info(f"**Induction:** {induction}")
246
- c2.info(f"**Fuel:** {fuel}")
247
- c3.info(f"**Camshafts:** {cams}")
248
- c4.info(f"**Nitrous:** {'Enabled' if nitrous else 'Disabled'}")
249
-
250
- if pct_gain > 50:
251
- st.toast("πŸš€ Massive gains detected! Check transmission limits.", icon="⚠️")
 
 
 
 
 
 
 
 
 
 
 
 
252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
 
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
 
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