-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathstreamlit_app.py
317 lines (257 loc) · 14 KB
/
streamlit_app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
import matplotlib
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
#import seaborn as sns
from datetime import datetime
from time import time
from datetime import timedelta
import warnings
import altair as alt
import streamlit as st
#constants
default_bike_weight = 17.0 #kg
sprung_mass_proportion_of_bike_weight = 0.75 # wheels tyres, cassette half swingarm, half fork.
default_system_sprung_mass = default_bike_weight*sprung_mass_proportion_of_bike_weight + 75 #bike + rider
default_weight_distribution_on_rear_wheel = 0.55
#functions
def motion_ratio(travel, stroke):
return travel/stroke
def spring_rate_at_wheel(spring, motion_ratio):
return spring/(motion_ratio**2)
def spring_rate_at_wheel_normlaised_75kg(spring_rate_at_wheel, system_weight):
return spring_rate_at_wheel*(default_system_sprung_mass) /system_weight
def energy_at_max_travel(spring_rate, shock_stroke):
return 0.5*(spring_rate*175.126835)*((shock_stroke/1000)**2)
def huck_height(energy_at_max_travel, weight):
return energy_at_max_travel/(default_weight_distribution_on_rear_wheel*weight*9.81)
def add_label(name, spring):
return name + " " + str(spring) + "lbs/in"
# Calculate required quantities
def add_calucated_quantitles(df, normalising_rider_weight, bike_weight, normallising_motion_ratio):
df['Motion_ratio'] = motion_ratio(df['Travel'], df['Stroke'])
df['Spring_rate_at_wheel'] = spring_rate_at_wheel(df['Spring_rate'],df['Motion_ratio'])
df['Spring_rate_at_wheel_normalised_75kg'] = spring_rate_at_wheel_normlaised_75kg(df['Spring_rate_at_wheel'],(df['Weight'] + sprung_mass_proportion_of_bike_weight * bike_weight))
df['Energy_at_max_travel'] = energy_at_max_travel(df['Spring_rate'], df['Stroke'])
df['Huck_height_(m)'] = huck_height(df['Energy_at_max_travel'], (df['Weight'] + sprung_mass_proportion_of_bike_weight * bike_weight))
df['LabelX'] = np.vectorize(add_label)(df['Name'], df['Spring_rate'])
df['Adjusted_spring_rate'] = df['Spring_rate_at_wheel_normalised_75kg']*((normalising_rider_weight + bike_weight*sprung_mass_proportion_of_bike_weight )/(default_system_sprung_mass)*normallising_motion_ratio**2)
# Title
st.markdown("<h1 style='font-size: 60px; font-family: Helvetica; font-weight: bold; margin-bottom: 0;'>Setup Analyzer</h1>", unsafe_allow_html=True)
st.markdown("<h2 style='font-size: 36px; font-family: Helvetica; margin-top: 0;'>Uncover your ideal configuration</h2>", unsafe_allow_html=True)
# Description
st.write("\n")
st.markdown("""
<sub style='font-size: 16px;'>
Enter your details on the left then fiddle with the spring rate and reach until you find the appropriate choices for your height, travel and weight.
</sub>
""", unsafe_allow_html=True)
# Credit
st.write("\n")
st.write("\n")
st.markdown("""
<sub style='font-size: 16px;'>
Credit: Rowland Jowett for the original very useful prototype.
</sub>
""", unsafe_allow_html=True)
st.write("\n")
st.write("\n")
st.write("\n")
# User Inputs
st.write("\n")
st.sidebar.markdown("### Setup Analyzer")
st.sidebar.markdown("Enter your setup...")
user_discipline = st.sidebar.selectbox("Discipline", ['Enduro', 'DH'])
user_stroke = st.sidebar.slider("Rear shock stroke (mm)", 20.0, 100.0, 65.0, 0.5) # All values are floats
user_travel = st.sidebar.slider("Rear wheel vertical travel (mm)", 100.0, 250.0, 160.0, 5.0) # All values are floats
user_weight = st.sidebar.slider("Rider weight (Kg)", 20.0, 200.0, 78.0, 1.0) # All values are floats
user_bike_weight = st.sidebar.slider("Bike weight (Kg)", 5.0, 30.0, default_bike_weight, 1.0) # All values are floats
user_spring_rate = st.sidebar.slider("Spring_rate lbs/in", 200.0, 800.0, 434.0, 5.0) # All values are floats
user_height = st.sidebar.slider("Rider Height (cm)", 110.0, 210.0, 181.0, 1.0) # All values are floats
user_bike_reach = st.sidebar.slider("Bicycle Reach (mm)", 300.0, 600.0, 480.0, 5.0) # All values are floats
user_speed_rating = st.sidebar.slider("Rider speed, Mens WCDH = 10", 1.0, 10.0, 5.0, 1.0) # All values are floats
user_name = st.sidebar.text_input("Name", "Jane Doe")
user_motion_ratio = motion_ratio(user_travel, user_stroke)
# Adding a visual distinction for the Display options section
st.sidebar.markdown("""
<hr style="height:2px;border:none;color:#333;background-color:#333;" />
<h3 style="background-color: #f0f2f6; padding: 10px; border-radius: 5px;">Display options</h3>
""", unsafe_allow_html=True)
# Display options
speed_rating_include = st.sidebar.slider("Speed rating of riders to exclude from analysis. 1: slowest, 10: pro", 1, 10, 1, 1)
display_normalised_reach = st.sidebar.selectbox("Display reach", [ 'Raw', 'Normalised'])
# Data Preparation and Calculation
# Your data reading and calculations here...
df = pd.read_csv("Data.csv", index_col=1)
df = df.reset_index()
df = df[df['Speed_rating'] >= speed_rating_include]
data = {
'Discipline': ['Entered data'],
'Name': [user_name],
'Stroke': [user_stroke],
'Travel': [user_travel],
'Spring_rate': [user_spring_rate],
'Weight': [user_weight],
'Speed_rating': [user_speed_rating],
}
df_user = pd.DataFrame(data)
add_calucated_quantitles(df, user_weight, default_bike_weight, user_motion_ratio)
add_calucated_quantitles(df_user, user_weight, user_bike_weight, user_motion_ratio)
df_user['plot_point_size'] = 3
df['plot_point_size'] = 2
df_combined = pd.concat([df, df_user])
# Make the chart
points = alt.Chart(df, title='Adjusted spring rate vs Bike Max Travel').mark_circle().encode(
alt.X('Travel:Q',axis=alt.Axis(title ='Travel')).scale(zero=False),
alt.Y('Adjusted_spring_rate:Q', axis=alt.Axis(title ='Adjusted spring rate')).scale(zero=False),
color=alt.Color('Discipline:N', scale=alt.Scale(domain=['Enduro', 'DH', 'Entered data', 'best fit line'], range=['blue', 'green', 'red', 'purple'])),
size='Speed_rating:Q',
tooltip=['Name', 'Weight', 'Spring_rate', 'Speed_rating', 'Discipline'],
)
points_user = alt.Chart(df_user, title='Adjusted spring rate vs Bike Max Travel').mark_circle().encode(
alt.X('Travel:Q',axis=alt.Axis(title ='Travel')).scale(zero=False),
alt.Y('Adjusted_spring_rate:Q', axis=alt.Axis(title ='Adjusted spring rate')).scale(zero=False),
color=alt.Color('Discipline:N', scale=alt.Scale(domain=['Enduro', 'DH', 'Entered data', 'best fit line'], range=['blue', 'green', 'red', 'purple'])),
tooltip=['Name', 'Weight', 'Spring_rate', 'Speed_rating', 'Discipline'],
size=alt.value(200)
)
labels = alt.Chart(df_combined).mark_text(align='left', baseline='middle', dx=4, fontSize=14).encode(alt.X('Travel:Q').scale(zero=False),
alt.Y('Adjusted_spring_rate:Q').scale(zero=False),
text='LabelX:N')
reg = alt.Chart(df).mark_circle().encode(
alt.X('Travel:Q',axis=alt.Axis(title ='Travel')).scale(zero=False),
alt.Y('Adjusted_spring_rate:Q', axis=alt.Axis(title ='Adjusted spring rate')).scale(zero=False),
color=alt.Color('Discipline:N', scale=alt.Scale(domain=['Enduro', 'DH', 'Entered data', 'best fit line'], range=['blue', 'green', 'red', 'purple'])),
size='Speed_rating:Q',
tooltip=['Name', 'Weight', 'Spring_rate', 'Speed_rating', 'Discipline']
).transform_regression('Travel', 'Adjusted_spring_rate').mark_line(
opacity=0.50,
shape='mark'
).transform_fold(
["best fit line"],
as_=["Regression", "y"]
).encode(alt.Color("Regression:N"))
charts = (points + points_user + reg + labels).properties(width="container").interactive().properties(
width=1000,
height=600
).configure_title(
fontSize=24
)
# Make the chart
huck_height_chart = alt.Chart(df, title='Huck height (m) vs Bike Max Travel').mark_circle().encode(
alt.X('Travel:Q').scale(zero=False),
alt.Y('Huck_height_(m):Q').scale(zero=False),
color=alt.Color('Discipline:N', scale=alt.Scale(domain=['Enduro', 'DH', 'Entered data', 'best fit line'], range=['blue', 'green', 'red', 'purple'])),
size='Speed_rating:Q',
tooltip=['Name', 'Weight', 'Spring_rate', 'Speed_rating', 'Discipline']
).properties(
width="container"
)
huck_height_chart_user = alt.Chart(df_user, title='Huck height (m) vs Bike Max Travel').mark_circle().encode(
alt.X('Travel:Q').scale(zero=False),
alt.Y('Huck_height_(m):Q').scale(zero=False),
color=alt.Color('Discipline:N', scale=alt.Scale(domain=['Enduro', 'DH', 'Entered data', 'best fit line'], range=['blue', 'green', 'red', 'purple'])),
size=alt.value(200),
tooltip=['Name', 'Weight', 'Spring_rate', 'Speed_rating', 'Discipline'],
).properties(
width="container"
)
labels_h = alt.Chart(df_combined).mark_text(align='left', baseline='middle', dx=4, fontSize=14).encode(alt.X('Travel:Q').scale(zero=False),
alt.Y('Huck_height_(m):Q').scale(zero=False),
text='LabelX:N')
reg_h = alt.Chart(df).mark_circle().encode(
alt.X('Travel:Q',axis=alt.Axis(title ='Travel')).scale(zero=False),
alt.Y('Huck_height_(m):Q', axis=alt.Axis(title ='Huck height (m)')).scale(zero=False),
color=alt.Color('Discipline:N', scale=alt.Scale(domain=['Enduro', 'DH', 'Entered data', 'best fit line'], range=['blue', 'green', 'red', 'purple'])),
tooltip=['Name', 'Weight', 'Spring_rate', 'Speed_rating', 'Discipline']
).transform_regression('Travel', 'Huck_height_(m)').mark_line(
opacity=0.50,
shape='mark'
).transform_fold(
["best fit line"],
as_=["Regression", "y"]
).encode(alt.Color("Regression:N"))
charts2 = (huck_height_chart + huck_height_chart_user + reg_h + labels_h).interactive().properties(
width=1000,
height=600
).configure_title(
fontSize=24
)
#now do normalised reach:
df_reach = pd.read_csv("Data_Reach.csv", index_col=1)
df_reach = df_reach.reset_index()
#filter to the selected speed rating
df_reach = df_reach[df_reach['Speed_rating'] >= speed_rating_include]
data_reach = {
'Name': [user_name],
'Height': [user_height],
'Discipline': ['Entered data'],
'Bike': [""],
'Reach': [user_bike_reach],
'Speed_rating': [user_speed_rating],
'Reach_Normalised' : [user_bike_reach],
}
df_reach['Reach_Normalised'] = df_reach['Reach']*(user_height/df_reach['Height'])
df_user_reach = pd.DataFrame(data_reach)
df_reach_combined = pd.concat([df_reach, df_user_reach])
# Make the chart
y_axis_encoding = 'Reach_Normalised:Q' if display_normalised_reach == 'Normalised' else 'Reach:Q'
y_regression_target = 'Reach_Normalised' if display_normalised_reach == 'Normalised' else 'Reach'
reach_title = 'Normalised Reach (mm)' if display_normalised_reach == 'Normalised' else 'Reach (mm)'
reach_chart = alt.Chart(df_reach, title=reach_title).mark_circle().encode(
alt.X('Height:Q', axis=alt.Axis(title ='Rider Height (cm)')).scale(zero=False),
alt.Y(y_axis_encoding, axis=alt.Axis(title =' Reach (mm)')).scale(zero=False),
color=alt.Color('Discipline:N', scale=alt.Scale(domain=['Enduro', 'DH', 'Entered data', 'best fit line'], range=['blue', 'green', 'red', 'purple'])),
size='Speed_rating:Q',
tooltip=['Name', 'Bike', 'Speed_rating', 'Discipline']
).properties(
width="container"
)
reach_chart_user = alt.Chart(df_user_reach, title=reach_title).mark_circle().encode(
alt.X('Height:Q').scale(zero=False),
alt.Y(y_axis_encoding).scale(zero=False),
color=alt.Color('Discipline:N', scale=alt.Scale(domain=['Enduro', 'DH', 'Entered data', 'best fit line'], range=['blue', 'green', 'red', 'purple'])),
size='Speed_rating:Q',
tooltip=['Name', 'Bike', 'Speed_rating', 'Discipline']
).properties(
width="container"
)
labels_r = alt.Chart(df_reach_combined).mark_text(align='left', baseline='middle', dx=4, fontSize=14).encode(alt.X('Height:Q').scale(zero=False),
alt.Y(y_axis_encoding).scale(zero=False),
text='Name:N')
reg_r = alt.Chart(df_reach, title=reach_title).mark_circle().encode(
alt.X('Height:Q').scale(zero=False),
alt.Y(y_axis_encoding).scale(zero=False),
color=alt.Color('Discipline:N', scale=alt.Scale(domain=['Enduro', 'DH', 'Entered data', 'best fit line'], range=['blue', 'green', 'red', 'purple'])),
size='Speed_rating:Q',
tooltip=['Name', 'Bike', 'Speed_rating', 'Discipline']
).transform_regression('Height', y_regression_target).mark_line(
opacity=0.50,
shape='mark'
).transform_fold(
["best fit line"],
as_=["Regression", "y"]
).encode(alt.Color("Regression:N"))
charts3 = (reach_chart + reach_chart_user + reg_r + labels_r).interactive().properties(
width=1000,
height=600
).configure_title(
fontSize=24
)
#display charts
# Display the chart in the Streamlit app
st.altair_chart(charts)
st.altair_chart(charts2)
st.altair_chart(charts3)
st.markdown("""
### Definitions:
- **Adjusted_spring_rate**
This metric adjusts the spring rate of different setups to make them comparable as if the entered rider were using them. By standardizing the setups you can easily compare the stiffness across different rider weights. For instance, if a 90kg rider uses a 500lbs/in spring, this would feel roughly the same as a 75kg rider using a 516lbs/in spring.
- **Huck_height:**
Huck_height is the height you could drop rider and bike from and all energy be contained in the spring without bottoming out (assumes 60% of the weight on the rear wheel)
- **Normalised Reach:**
This divides the reach of the bike by rider height and then multiplies by the entered rider data height to get a comparable bike reach. This is a good way to compare the reach of different bikes for a given rider height.
Full calculations here: https://github.com/wgm20/MTBCoilSpringRateAnalyzer/blob/main/CoilSpringRateComparisons_Streamlit.py
Any questions, please email [mulholland.william@gmail.com](mailto:mulholland.william@gmail.com) with the subject: Coil spring rate comparisons.
If this page is of more than fleeting interest, please consider donating to the air ambulance service: [Air Ambulance Donation](https://theairambulanceservice.org.uk)
""")