MaxBDKT commited on
Commit
84ed130
·
verified ·
1 Parent(s): 7a8b574

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +72 -99
src/streamlit_app.py CHANGED
@@ -5,28 +5,28 @@ import plotly.graph_objects as go
5
  import os
6
 
7
  # ==============================================================================
8
- # 1. CONFIGURATION ET STYLE CSS (CONTRASTE MAXIMUM & VISIBILITÉ INPUTS)
9
  # ==============================================================================
10
  st.set_page_config(page_title="Brake Performance Lab", layout="wide", page_icon="🚲")
11
 
12
  st.markdown("""
13
  <style>
14
- /* 1.1 Fond blanc pur et texte noir global */
15
  .stApp, [data-testid="stSidebar"] { background-color: #FFFFFF !important; }
16
  * { color: #000000 !important; font-family: 'Arial', sans-serif; }
17
 
18
- /* 1.2 Sidebar ultra-compacte */
19
  [data-testid="stSidebar"] [data-testid="stVerticalBlock"] { gap: 0.1rem !important; padding-top: 0rem !important; }
20
  hr { margin: 0.5rem 0 !important; }
21
 
22
- /* 1.3 Fix pour les Listes Déroulantes (Anti-Dark Mode) */
23
  div[data-baseweb="select"] { border: 2px solid #000 !important; background-color: #FFF !important; }
24
  div[data-baseweb="select"] * { color: #000 !important; }
25
  ul[role="listbox"] { background-color: #FFFFFF !important; border: 2px solid #000 !important; }
26
  li[role="option"] { color: #000 !important; font-weight: bold !important; background-color: #FFF !important; }
27
  li[role="option"]:hover { background-color: #0082C3 !important; color: #FFF !important; }
28
 
29
- /* 1.4 TA DEMANDE : TEXTE BLANC DANS LES INPUTS DE MASSE/VITESSE */
30
  input[type="number"], div[data-baseweb="input"] input {
31
  color: #FFFFFF !important;
32
  background-color: #333333 !important;
@@ -34,102 +34,104 @@ st.markdown("""
34
  font-weight: bold !important;
35
  }
36
 
37
- /* 1.5 Tags multiselect bleu Decathlon */
38
  [data-testid="stMultiSelect"] span { background-color: #0082C3 !important; color: #FFFFFF !important; }
39
 
40
- /* 1.6 Style des boites de Dashboard Analyse */
41
  [data-testid="column"] {
42
  padding: 12px !important; border: 2px solid #000 !important;
43
  border-radius: 8px !important; background-color: #FFFFFF !important;
44
  margin-bottom: 10px;
45
  }
46
- [data-testid="stMetricValue"] { font-weight: 800 !important; font-size: 22px !important; }
47
 
48
- /* 1.7 Alertes de conformité */
49
- .alert-red { color: #B71C1C !important; font-weight: 900 !important; font-size: 12px; margin-top: 5px; border: 1px solid #B71C1C; padding: 4px; border-radius: 4px; }
50
- .check-green { color: #1B5E20 !important; font-weight: 900 !important; font-size: 12px; margin-top: 5px; }
51
  </style>
52
  """, unsafe_allow_html=True)
53
 
54
  # ==============================================================================
55
- # 2. CHARGEMENT DES DONNÉES EXCEL
56
  # ==============================================================================
57
  @st.cache_data
58
  def load_data():
59
  current_dir = os.path.dirname(__file__)
60
  file_path = os.path.join(current_dir, "Brake_Lab_Test_Data.xlsx")
61
- if not os.path.exists(file_path):
62
- return pd.DataFrame()
63
  df = pd.read_excel(file_path, sheet_name='Data')
64
  df.columns = df.columns.str.strip()
65
  return df
66
 
67
  df = load_data()
68
- if df.empty:
69
- st.error("Erreur : Fichier Excel introuvable.")
70
- st.stop()
71
 
72
  try:
73
  all_models = df['model name'].unique().tolist()
74
 
75
  # ==========================================================================
76
- # 3. SIDEBAR : PARAMÉTRAGE COMPLET (SANS AUCUN RETRAIT)
77
  # ==========================================================================
78
  with st.sidebar:
79
  st.image("https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Decathlon_Logo.svg/1280px-Decathlon_Logo.svg.png", width=150)
80
  st.title("⚙️ SETTINGS")
81
 
82
- # 3.1 Effort principal
83
- x_input = st.slider("🫱 Lever Effort [N]", 40, 200, 100)
84
- selected_models = st.multiselect("Select Models to Display", options=all_models, default=all_models[:2])
85
 
86
- # 3.2 Normes
87
- st.subheader("📋 Standard Compliance")
88
  norm_type = st.selectbox("Category", ["None", "City/Trekking", "Kids", "MTB", "Racing"])
89
 
90
- # 3.3 Simulation Freinage (Vitesse, Masse, Répartition)
91
  st.markdown("---")
92
- st.subheader("📏 Braking Simulation")
93
- v_kmh = st.number_input("Bike Speed (km/h)", value=25, min_value=1)
94
- m_total = st.number_input("Total Mass (kg)", value=100, min_value=1)
95
- mass_front_pct = st.slider("Mass Balance Front (%)", 0, 100, 70)
96
- st.caption(f"Calculated Rear Balance: {100 - mass_front_pct}%")
97
 
98
- # 3.4 Coefficients de frottement
99
- mu_dry = st.slider("Friction Coeff (Dry)", 0.1, 1.2, 0.8)
100
- mu_wet = st.slider("Friction Coeff (Wet)", 0.1, 1.2, 0.4)
101
-
102
- # 3.5 Options d'affichage (Benchmark & Loss)
103
- st.markdown("---")
104
- with st.expander("🔍 Display Options"):
105
- show_loss = st.checkbox("Show Wet Loss (%)", value=True)
106
- enable_comparison = st.checkbox("Enable Reference (Vs Ref)", value=True)
107
- ref_model = st.selectbox("Benchmark Reference", options=all_models)
 
 
108
  condition_view = st.radio("View Mode", ["Both", "Dry only", "Wet only"], index=0)
109
 
110
  # ==========================================================================
111
- # 4. CALCULS LOGIQUES (NORMES & RÉFÉRENCE)
112
  # ==========================================================================
113
- # 4.1 Seuils de Normes
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  n_dry, n_wet = 0, 0
115
  if norm_type == "City/Trekking": n_dry, n_wet = 340, 220
116
  elif norm_type == "Kids": n_dry, n_wet = 204, 132
117
  elif norm_type == "MTB": n_dry, n_wet = 425, 280
118
  elif norm_type == "Racing": n_dry, n_wet = 425, 260
119
 
120
- # 4.2 Valeurs de référence pour les deltas
121
  row_ref = df[df['model name'] == ref_model].iloc[0]
122
  ref_d_val = row_ref['dry a'] * x_input + row_ref['dry b']
123
  ref_w_val = row_ref['wet a'] * x_input + row_ref['wet b']
124
 
125
- # ==========================================================================
126
- # 5. HEADER : DIAGNOSTIC VISUEL
127
- # ==========================================================================
128
  label, color = ("❄️ LIGHT", "#a1c4fd") if x_input < 70 else (("⚖️ MODERATE", "#ffdb58") if x_input <= 110 else ("🔥 POWERFUL", "#ff4b4b"))
129
- st.markdown(f"<div style='background-color:{color}; padding:8px; border-radius:8px; text-align:center; border: 3px solid #000; margin-bottom: 15px;'><span style='font-weight:900; font-size:16px;'>{label} BRAKING | Applied Effort: {x_input} N</span></div>", unsafe_allow_html=True)
130
 
131
  # ==========================================================================
132
- # 6. GRAPHIQUE INTERACTIF (INTERSECTIONS X ET COURBES)
133
  # ==========================================================================
134
  filtered_df = df[df['model name'].isin(selected_models)]
135
  fig = go.Figure()
@@ -142,79 +144,50 @@ try:
142
  y_d, y_w = (row['dry a'] * x_input + row['dry b']), (row['wet a'] * x_input + row['wet b'])
143
  results_data.append({"name": row['model name'], "dry": y_d, "wet": y_w, "row": row})
144
 
145
- # Courbes Dry
146
  if condition_view in ["Both", "Dry only"]:
147
  fig.add_trace(go.Scatter(x=x_range, y=row['dry a']*x_range + row['dry b'], name=f"{row['model name']} (D)", line=dict(color=c, width=4)))
148
  if n_dry > 0:
149
  xt = (n_dry - row['dry b']) / row['dry a']
150
- if xt <= 200:
151
- fig.add_trace(go.Scatter(x=[xt], y=[n_dry], mode='markers+text', text=[f"{round(xt,1)}N"], textfont=dict(color="black", weight=700), textposition="top center", marker=dict(color=c, size=10, symbol='x'), showlegend=False))
152
 
153
- # Courbes Wet
154
  if condition_view in ["Both", "Wet only"]:
155
  fig.add_trace(go.Scatter(x=x_range, y=row['wet a']*x_range + row['wet b'], name=f"{row['model name']} (W)", line=dict(color=c, width=2, dash='dot')))
156
- if n_wet > 0:
157
- xtw = (n_wet - row['wet b']) / row['wet a']
158
- if xtw <= 200:
159
- fig.add_trace(go.Scatter(x=[xtw], y=[n_wet], mode='markers+text', text=[f"{round(xtw,1)}N"], textfont=dict(color="black", weight=700), textposition="bottom center", marker=dict(color=c, size=10, symbol='circle-open'), showlegend=False))
160
-
161
- # Lignes de Normes Horizontales
162
- if n_dry > 0 and (condition_view in ["Both", "Dry only"]):
163
- fig.add_hline(y=n_dry, line_width=3, line_color="#000", annotation_text="Norm Dry")
164
- if n_wet > 0 and (condition_view in ["Both", "Wet only"]):
165
- fig.add_hline(y=n_wet, line_width=3, line_dash="dot", line_color="#000", annotation_text="Norm Wet")
166
-
167
- # Ligne d'effort vertical
168
- fig.add_vline(x=x_input, line_width=2, line_dash="dash", line_color="#000")
169
-
170
- # Mise en page (Fix Plotly title)
171
- fig.update_layout(height=480, plot_bgcolor='white', paper_bgcolor='white', hovermode="x unified",
172
- xaxis=dict(title=dict(text="Lever Effort [N]", font=dict(color="black", weight=700)), tickfont=dict(color="black", weight=700), linecolor="black", gridcolor="#EEE"),
173
- yaxis=dict(title=dict(text="Performance [N]", font=dict(color="black", weight=700)), tickfont=dict(color="black", weight=700), linecolor="black", gridcolor="#EEE"),
174
- legend=dict(font=dict(color="black", weight=700), bordercolor="black", borderwidth=1, bgcolor="white"))
175
  st.plotly_chart(fig, use_container_width=True)
176
 
177
  # ==========================================================================
178
- # 7. DASHBOARD : ANALYSE DÉTAILLÉE & SIMULATION STOP DISTANCE
179
  # ==========================================================================
180
- st.markdown(f"**📊 System Performance Analysis | Speed: {v_kmh}km/h | Mass: {m_total}kg**")
181
-
182
  cols = st.columns(len(results_data))
183
- v_ms = v_kmh / 3.6
184
- energy = 0.5 * m_total * (v_ms**2)
185
- r_av, r_ar = mass_front_pct / 100, (100 - mass_front_pct) / 100
186
 
187
  for i, res in enumerate(results_data):
188
  with cols[i]:
189
- is_ref = (res['name'] == ref_model)
190
- st.markdown(f"<u>**{res['name']}**</u> {'⭐' if is_ref else ''}", unsafe_allow_html=True)
191
 
192
- # 7.1 Analyse SEC (Force cumulée AV+AR)
193
  if condition_view in ["Both", "Dry only"]:
194
- d_p = round(res['dry'], 1)
195
- st.metric("Dry Perf.", f"{d_p} N", f"{round(d_p - ref_d_val, 1)} N vs Ref" if enable_comparison and not is_ref else None)
196
- f_tot_d = (res['dry'] * mu_dry * r_av) + (res['dry'] * mu_dry * r_ar)
197
- st.write(f"🛑 **Stop (Dry): {round(energy/f_tot_d, 2) if f_tot_d > 0 else 0} m**")
 
198
  if n_dry > 0:
199
  xt = (n_dry - res['row']['dry b']) / res['row']['dry a']
200
- if xt > 180: st.markdown(f"<div class='alert-red'>NON CONFORME SEC ({round(xt,1)}N)</div>", unsafe_allow_html=True)
201
- else: st.markdown(f"<div class='check-green'> Conforme Sec</div>", unsafe_allow_html=True)
202
-
203
- # 7.2 Analyse HUMIDE (Force cumulée AV+AR)
204
  if condition_view in ["Both", "Wet only"]:
205
- w_p = round(res['wet'], 1)
206
- st.metric("Wet Perf.", f"{w_p} N", f"{round(w_p - ref_w_val, 1)} N vs Ref" if enable_comparison and not is_ref else None)
207
- f_tot_w = (res['wet'] * mu_wet * r_av) + (res['wet'] * mu_wet * r_ar)
208
- st.write(f"🌧️ **Stop (Wet): {round(energy/f_tot_w, 2) if f_tot_w > 0 else 0} m**")
209
  if n_wet > 0:
210
  xtw = (n_wet - res['row']['wet b']) / res['row']['wet a']
211
- if xtw > 180: st.markdown(f"<div class='alert-red'>NON CONFORME HUMIDE ({round(xtw,1)}N)</div>", unsafe_allow_html=True)
212
- else: st.markdown(f"<div class='check-green'> Conforme Humide</div>", unsafe_allow_html=True)
213
-
214
- # 7.3 Perte d'efficacité
215
- if show_loss and condition_view == "Both":
216
- loss = ((res['dry'] - res['wet']) / res['dry'] * 100) if res['dry'] != 0 else 0
217
- st.metric("Wet Loss", f"-{round(loss, 1)}%", f"{round(res['wet']-res['dry'], 1)} N abs.", delta_color="inverse")
218
 
219
  except Exception as e:
220
- st.error(f"Une erreur est survenue : {e}")
 
5
  import os
6
 
7
  # ==============================================================================
8
+ # 1. CONFIGURATION ET STYLE CSS (HAUT CONTRASTE & INPUTS)
9
  # ==============================================================================
10
  st.set_page_config(page_title="Brake Performance Lab", layout="wide", page_icon="🚲")
11
 
12
  st.markdown("""
13
  <style>
14
+ /* 1.1 Thème Global Blanc */
15
  .stApp, [data-testid="stSidebar"] { background-color: #FFFFFF !important; }
16
  * { color: #000000 !important; font-family: 'Arial', sans-serif; }
17
 
18
+ /* 1.2 Sidebar & Interactivité */
19
  [data-testid="stSidebar"] [data-testid="stVerticalBlock"] { gap: 0.1rem !important; padding-top: 0rem !important; }
20
  hr { margin: 0.5rem 0 !important; }
21
 
22
+ /* 1.3 Listes déroulantes (Fix Noir/Noir) */
23
  div[data-baseweb="select"] { border: 2px solid #000 !important; background-color: #FFF !important; }
24
  div[data-baseweb="select"] * { color: #000 !important; }
25
  ul[role="listbox"] { background-color: #FFFFFF !important; border: 2px solid #000 !important; }
26
  li[role="option"] { color: #000 !important; font-weight: bold !important; background-color: #FFF !important; }
27
  li[role="option"]:hover { background-color: #0082C3 !important; color: #FFF !important; }
28
 
29
+ /* 1.4 INPUTS NUMÉRIQUES (Texte blanc sur fond gris foncé) */
30
  input[type="number"], div[data-baseweb="input"] input {
31
  color: #FFFFFF !important;
32
  background-color: #333333 !important;
 
34
  font-weight: bold !important;
35
  }
36
 
37
+ /* 1.5 Tags multiselect */
38
  [data-testid="stMultiSelect"] span { background-color: #0082C3 !important; color: #FFFFFF !important; }
39
 
40
+ /* 1.6 Dashboard Analyse */
41
  [data-testid="column"] {
42
  padding: 12px !important; border: 2px solid #000 !important;
43
  border-radius: 8px !important; background-color: #FFFFFF !important;
44
  margin-bottom: 10px;
45
  }
 
46
 
47
+ /* 1.7 Alertes Normes (Texte blanc sur badges) */
48
+ .alert-red { color: #FFFFFF !important; background-color: #B71C1C !important; font-weight: 900 !important; padding: 6px; border-radius: 4px; text-align: center; }
49
+ .check-green { color: #FFFFFF !important; background-color: #1B5E20 !important; font-weight: 900 !important; padding: 6px; border-radius: 4px; text-align: center; }
50
  </style>
51
  """, unsafe_allow_html=True)
52
 
53
  # ==============================================================================
54
+ # 2. CHARGEMENT DES DONNÉES
55
  # ==============================================================================
56
  @st.cache_data
57
  def load_data():
58
  current_dir = os.path.dirname(__file__)
59
  file_path = os.path.join(current_dir, "Brake_Lab_Test_Data.xlsx")
60
+ if not os.path.exists(file_path): return pd.DataFrame()
 
61
  df = pd.read_excel(file_path, sheet_name='Data')
62
  df.columns = df.columns.str.strip()
63
  return df
64
 
65
  df = load_data()
66
+ if df.empty: st.error("Fichier Excel introuvable."); st.stop()
 
 
67
 
68
  try:
69
  all_models = df['model name'].unique().tolist()
70
 
71
  # ==========================================================================
72
+ # 3. SIDEBAR (NAVIGATION & PARAMÈTRES)
73
  # ==========================================================================
74
  with st.sidebar:
75
  st.image("https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Decathlon_Logo.svg/1280px-Decathlon_Logo.svg.png", width=150)
76
  st.title("⚙️ SETTINGS")
77
 
78
+ # Effort Levier (Input principal)
79
+ x_input = st.slider("🫱 Effort Levier [N]", 40, 200, 100)
80
+ selected_models = st.multiselect("Models", options=all_models, default=all_models[:2])
81
 
82
+ st.subheader("📋 Compliance")
 
83
  norm_type = st.selectbox("Category", ["None", "City/Trekking", "Kids", "MTB", "Racing"])
84
 
 
85
  st.markdown("---")
 
 
 
 
 
86
 
87
+ # Simulation Dynamique en Expandable
88
+ with st.expander("📏 Braking Simulation", expanded=False):
89
+ v_kmh = st.number_input("Speed (km/h)", value=25)
90
+ m_total = st.number_input("Total Mass (kg)", value=100)
91
+ rim_inch = st.selectbox("Wheel Size (inch)", [20, 24, 26, 27.5, 28, 29], index=4)
92
+ dist_av_pct = st.slider("Sharing Front (%)", 0, 100, 70)
93
+ mu_dry = st.slider("Friction (Dry ground)", 0.1, 1.2, 0.8)
94
+ mu_wet = st.slider("Friction (Wet ground)", 0.1, 1.2, 0.4)
95
+
96
+ # Options d'affichage
97
+ with st.expander("🔍 Display Options", expanded=False):
98
+ ref_model = st.selectbox("Reference Benchmark", options=all_models)
99
  condition_view = st.radio("View Mode", ["Both", "Dry only", "Wet only"], index=0)
100
 
101
  # ==========================================================================
102
+ # 4. LOGIQUE DE CALCULS PHYSIQUES (UNITÉS S.I.)
103
  # ==========================================================================
104
+ # 4.1 Conversions
105
+ r_wheel = (rim_inch * 0.0254) / 2 # Rayon en mètres
106
+ v_ms = v_kmh / 3.6 # Vitesse en m/s
107
+
108
+ # 4.2 Inertie (Approximation jante+pneu = 1.5kg par roue)
109
+ # Formule : J = m * R^2
110
+ j_wheel = 1.5 * (r_wheel**2)
111
+ w_speed = v_ms / r_wheel # Vitesse angulaire (rad/s)
112
+
113
+ # 4.3 Énergie Totale (Translation + Rotation des 2 roues)
114
+ # E = 1/2mv^2 + 2 * (1/2 Jw^2)
115
+ energy_total = (0.5 * m_total * (v_ms**2)) + (2 * (0.5 * j_wheel * (w_speed**2)))
116
+
117
+ # 4.4 Normes
118
  n_dry, n_wet = 0, 0
119
  if norm_type == "City/Trekking": n_dry, n_wet = 340, 220
120
  elif norm_type == "Kids": n_dry, n_wet = 204, 132
121
  elif norm_type == "MTB": n_dry, n_wet = 425, 280
122
  elif norm_type == "Racing": n_dry, n_wet = 425, 260
123
 
124
+ # 4.5 Benchmark
125
  row_ref = df[df['model name'] == ref_model].iloc[0]
126
  ref_d_val = row_ref['dry a'] * x_input + row_ref['dry b']
127
  ref_w_val = row_ref['wet a'] * x_input + row_ref['wet b']
128
 
129
+ # Header Diagnostic
 
 
130
  label, color = ("❄️ LIGHT", "#a1c4fd") if x_input < 70 else (("⚖️ MODERATE", "#ffdb58") if x_input <= 110 else ("🔥 POWERFUL", "#ff4b4b"))
131
+ st.markdown(f"<div style='background-color:{color}; padding:8px; border-radius:8px; text-align:center; border: 3px solid #000; margin-bottom: 10px;'><span style='font-weight:900;'>{label} BRAKING | {x_input} N</span></div>", unsafe_allow_html=True)
132
 
133
  # ==========================================================================
134
+ # 5. GRAPHIQUE (DYNAMIQUE)
135
  # ==========================================================================
136
  filtered_df = df[df['model name'].isin(selected_models)]
137
  fig = go.Figure()
 
144
  y_d, y_w = (row['dry a'] * x_input + row['dry b']), (row['wet a'] * x_input + row['wet b'])
145
  results_data.append({"name": row['model name'], "dry": y_d, "wet": y_w, "row": row})
146
 
 
147
  if condition_view in ["Both", "Dry only"]:
148
  fig.add_trace(go.Scatter(x=x_range, y=row['dry a']*x_range + row['dry b'], name=f"{row['model name']} (D)", line=dict(color=c, width=4)))
149
  if n_dry > 0:
150
  xt = (n_dry - row['dry b']) / row['dry a']
151
+ if xt <= 200: fig.add_trace(go.Scatter(x=[xt], y=[n_dry], mode='markers', marker=dict(color=c, size=10, symbol='x'), showlegend=False))
 
152
 
 
153
  if condition_view in ["Both", "Wet only"]:
154
  fig.add_trace(go.Scatter(x=x_range, y=row['wet a']*x_range + row['wet b'], name=f"{row['model name']} (W)", line=dict(color=c, width=2, dash='dot')))
155
+
156
+ fig.update_layout(height=450, plot_bgcolor='white', paper_bgcolor='white', xaxis=dict(linecolor="black", gridcolor="#EEE"), yaxis=dict(linecolor="black", gridcolor="#EEE"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  st.plotly_chart(fig, use_container_width=True)
158
 
159
  # ==========================================================================
160
+ # 6. DASHBOARD ANALYSE (SIMULATION RÉELLE)
161
  # ==========================================================================
162
+ st.markdown(f"**📊 Dynamics Analysis | {v_kmh}km/h -> {round(v_ms,2)}m/s | Energy: {int(energy_total)} J**")
 
163
  cols = st.columns(len(results_data))
 
 
 
164
 
165
  for i, res in enumerate(results_data):
166
  with cols[i]:
167
+ st.markdown(f"<u>**{res['name']}**</u>", unsafe_allow_html=True)
 
168
 
169
+ # --- DRY ANALYSIS ---
170
  if condition_view in ["Both", "Dry only"]:
171
+ st.metric("Dry Perf.", f"{round(res['dry'], 1)} N")
172
+ # Force totale effective cumulée AV+AR au sol
173
+ f_eff_d = res['dry'] * mu_dry
174
+ d_stop_d = energy_total / f_eff_d if f_eff_d > 0 else 0
175
+ st.write(f"🛑 **Stop (Dry): {round(d_stop_d, 2)} m**")
176
  if n_dry > 0:
177
  xt = (n_dry - res['row']['dry b']) / res['row']['dry a']
178
+ if xt > 180: st.markdown(f"<div class='alert-red'>NON CONFORME SEC</div>", unsafe_allow_html=True)
179
+ else: st.markdown(f"<div class='check-green'>CONFORME SEC</div>", unsafe_allow_html=True)
180
+
181
+ # --- WET ANALYSIS ---
182
  if condition_view in ["Both", "Wet only"]:
183
+ st.metric("Wet Perf.", f"{round(res['wet'], 1)} N")
184
+ f_eff_w = res['wet'] * mu_wet
185
+ d_stop_w = energy_total / f_eff_w if f_eff_w > 0 else 0
186
+ st.write(f"🌧️ **Stop (Wet): {round(d_stop_w, 2)} m**")
187
  if n_wet > 0:
188
  xtw = (n_wet - res['row']['wet b']) / res['row']['wet a']
189
+ if xtw > 180: st.markdown(f"<div class='alert-red'>NON CONFORME HUMIDE</div>", unsafe_allow_html=True)
190
+ else: st.markdown(f"<div class='check-green'>CONFORME HUMIDE</div>", unsafe_allow_html=True)
 
 
 
 
 
191
 
192
  except Exception as e:
193
+ st.error(f"Erreur système : {e}")