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

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +192 -125
src/streamlit_app.py CHANGED
@@ -5,28 +5,51 @@ import plotly.graph_objects as go
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,160 +57,204 @@ st.markdown("""
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()
138
- x_range = np.linspace(40, 200, 150)
139
- colors = ['#0082C3', '#E63312', '#333333', '#00A14B', '#FFD200']
140
 
141
- results_data = []
142
  for i, (idx, row) in enumerate(filtered_df.iterrows()):
143
- c = colors[i % len(colors)]
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}")
 
5
  import os
6
 
7
  # ==============================================================================
8
+ # 1. CONFIGURATION ET STYLE CSS (FORÇAGE CONTRASTE TOTAL & ANTI-DARK MODE)
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 de base blanc pur */
15
+ .stApp, [data-testid="stSidebar"] {
16
+ background-color: #FFFFFF !important;
17
+ }
18
+
19
+ /* 1.2 Force le texte en NOIR PUR partout (Légendes, Labels, Titres) */
20
+ * {
21
+ color: #000000 !important;
22
+ font-family: 'Arial', sans-serif;
23
+ }
24
 
25
+ /* 1.3 Compactage Sidebar */
26
+ [data-testid="stSidebar"] [data-testid="stVerticalBlock"] {
27
+ gap: 0.1rem !important;
28
+ padding-top: 0rem !important;
29
+ }
30
+ hr { border-top: 1px solid #000 !important; margin: 0.5rem 0 !important; }
31
+
32
+ /* 1.4 DROPDOWNS : Correction Noir sur Noir pour Hugging Face */
33
+ div[data-baseweb="select"], div[role="combobox"] {
34
+ border: 2px solid #000000 !important;
35
+ background-color: #FFFFFF !important;
36
+ }
37
+ div[data-baseweb="select"] * { color: #000000 !important; }
38
+ ul[role="listbox"] {
39
+ background-color: #FFFFFF !important;
40
+ border: 2px solid #000000 !important;
41
+ }
42
+ li[role="option"] {
43
+ color: #000000 !important;
44
+ font-weight: bold !important;
45
+ background-color: #FFFFFF !important;
46
+ }
47
+ li[role="option"]:hover {
48
+ background-color: #0082C3 !important;
49
+ color: #FFFFFF !important;
50
+ }
51
+
52
+ /* 1.5 INPUTS NUMÉRIQUES (TA DEMANDE : Texte BLANC sur fond sombre) */
53
  input[type="number"], div[data-baseweb="input"] input {
54
  color: #FFFFFF !important;
55
  background-color: #333333 !important;
 
57
  font-weight: bold !important;
58
  }
59
 
60
+ /* 1.6 Correction des étiquettes Streamlit (Radios, Expanders, Labels) */
61
+ .st-emotion-cache-p4mowd, .st-emotion-cache-16idsys p, label, .st-ae {
62
+ color: #000000 !important;
63
+ font-weight: bold !important;
64
+ }
65
+
66
+ /* 1.7 Tags Multiselect (Bleu Decathlon) */
67
+ [data-testid="stMultiSelect"] span {
68
+ background-color: #0082C3 !important;
69
+ color: #FFFFFF !important;
70
+ }
71
 
72
+ /* 1.8 Boites du Dashboard Analyse */
73
  [data-testid="column"] {
74
+ padding: 15px !important;
75
+ border: 2px solid #000000 !important;
76
+ border-radius: 10px !important;
77
+ background-color: #FFFFFF !important;
78
  }
79
+ [data-testid="stMetricValue"] { font-weight: 900 !important; font-size: 24px !important; }
80
 
81
+ /* 1.9 BADGES NORMES (TA DEMANDE : Texte BLANC sur fond couleur) */
82
+ .alert-red {
83
+ color: #FFFFFF !important;
84
+ background-color: #B71C1C !important;
85
+ font-weight: 900 !important;
86
+ padding: 8px;
87
+ border-radius: 5px;
88
+ text-align: center;
89
+ margin-top: 5px;
90
+ }
91
+ .check-green {
92
+ color: #FFFFFF !important;
93
+ background-color: #1B5E20 !important;
94
+ font-weight: 900 !important;
95
+ padding: 8px;
96
+ border-radius: 5px;
97
+ text-align: center;
98
+ margin-top: 5px;
99
+ }
100
  </style>
101
  """, unsafe_allow_html=True)
102
 
103
  # ==============================================================================
104
+ # 2. ACCÈS AUX DONNÉES
105
  # ==============================================================================
106
  @st.cache_data
107
  def load_data():
108
  current_dir = os.path.dirname(__file__)
109
  file_path = os.path.join(current_dir, "Brake_Lab_Test_Data.xlsx")
110
+ if not os.path.exists(file_path):
111
+ return pd.DataFrame()
112
  df = pd.read_excel(file_path, sheet_name='Data')
113
  df.columns = df.columns.str.strip()
114
  return df
115
 
116
  df = load_data()
117
+ if df.empty:
118
+ st.error("Fichier Excel introuvable.")
119
+ st.stop()
120
 
121
  try:
122
  all_models = df['model name'].unique().tolist()
123
 
124
+ # ==============================================================================
125
+ # 3. SIDEBAR (CONTRÔLE TOTAL)
126
+ # ==============================================================================
127
  with st.sidebar:
128
  st.image("https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Decathlon_Logo.svg/1280px-Decathlon_Logo.svg.png", width=150)
129
+ st.title("⚙️ CONFIGURATION")
130
 
131
+ # 3.1 Entrée d'effort au levier
132
+ effort_x = st.slider("🫱 Effort Levier [N]", 40, 200, 100)
133
+ selected_models = st.multiselect("Modèles", options=all_models, default=all_models[:2])
134
 
135
+ # 3.2 Normes
136
+ st.subheader("📋 Normes de conformité")
137
+ norm_cat = st.selectbox("Catégorie", ["Aucune", "City/Trekking", "Kids", "MTB", "Racing"])
138
 
139
  st.markdown("---")
140
 
141
+ # 3.3 Simulation (TA DEMANDE : En déroulant)
142
  with st.expander("📏 Braking Simulation", expanded=False):
143
+ v_kmh = st.number_input("Vitesse (km/h)", value=25)
144
+ m_total = st.number_input("Masse Totale (kg)", value=100)
145
+ rim_inch = st.selectbox("Taille de roue (inch)", [20, 24, 26, 27.5, 28, 29], index=4)
146
+ sharing_av = st.slider("Sharing Front (%)", 0, 100, 70)
147
+ st.caption(f"Sharing Rear: {100 - sharing_av}%")
148
+
149
+ # 3.4 Options d'affichage (TA DEMANDE : En déroulant)
150
+ with st.expander("🔍 Options d'affichage", expanded=False):
151
+ ref_model_name = st.selectbox("Modèle de Benchmark", options=all_models)
152
+ view_mode = st.radio("Mode de vue", ["Sec & Humide", "Sec uniquement", "Humide uniquement"], index=0)
153
+ show_loss = st.checkbox("Calculer Perte Wet (%)", value=True)
154
+
155
+ # ==============================================================================
156
+ # 4. MOTEUR PHYSIQUE (CONVERSIONS S.I. & INERTIE)
157
+ # ==============================================================================
158
+ # 4.1 Unités S.I.
159
+ v_ms = v_kmh / 3.6 # km/h vers m/s
160
+ r_m = (rim_inch * 0.0254) / 2 # inch vers mètres
 
 
 
 
 
161
 
162
+ # 4.2 Énergie Totale (E_trans + E_rot)
163
+ # Estimation masse rotative (jante+pneu) = 1.5kg par roue
164
+ j_wheel = 1.5 * (r_m ** 2)
165
+ omega = v_ms / r_m
166
+ e_total_j = (0.5 * m_total * (v_ms**2)) + (2 * (0.5 * j_wheel * (omega**2)))
167
 
168
+ # 4.3 Seuils Normes
169
+ n_s, n_w = 0, 0
170
+ if norm_cat == "City/Trekking": n_s, n_w = 340, 220
171
+ elif norm_cat == "Kids": n_s, n_w = 204, 132
172
+ elif norm_cat == "MTB": n_s, n_w = 425, 280
173
+ elif norm_cat == "Racing": n_s, n_w = 425, 260
174
+
175
+ # 4.4 Référence Benchmark
176
+ row_bench = df[df['model name'] == ref_model_name].iloc[0]
177
+ ref_f_sec = row_bench['dry a'] * effort_x + row_bench['dry b']
178
+ ref_f_wet = row_bench['wet a'] * effort_x + row_bench['wet b']
179
+
180
+ # 5. HEADER DIAGNOSTIC
181
+ label, color = ("❄️ LIGHT", "#a1c4fd") if effort_x < 70 else (("⚖️ MODERATE", "#ffdb58") if effort_x <= 110 else ("🔥 POWERFUL", "#ff4b4b"))
182
+ 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 | Effort: {effort_x} N</span></div>", unsafe_allow_html=True)
183
+
184
+ # ==============================================================================
185
+ # 6. GRAPHIQUE (FIX COULEURS TEXTE NOIR)
186
+ # ==============================================================================
187
  filtered_df = df[df['model name'].isin(selected_models)]
188
  fig = go.Figure()
189
+ x_grid = np.linspace(40, 200, 150)
190
+ color_palette = ['#0082C3', '#E63312', '#333333', '#00A14B', '#FFD200']
191
 
192
+ results_summary = []
193
  for i, (idx, row) in enumerate(filtered_df.iterrows()):
194
+ clr = color_palette[i % len(color_palette)]
195
+ f_sec = row['dry a'] * effort_x + row['dry b']
196
+ f_wet = row['wet a'] * effort_x + row['wet b']
197
+ results_summary.append({"name": row['model name'], "dry": f_sec, "wet": f_wet, "row": row})
198
 
199
+ if view_mode != "Humide uniquement":
200
+ fig.add_trace(go.Scatter(x=x_grid, y=row['dry a']*x_grid + row['dry b'], name=f"{row['name']} (D)", line=dict(color=clr, width=4)))
201
+ if n_s > 0:
202
+ xt = (n_s - row['dry b']) / row['dry a']
203
+ if 40 <= xt <= 200:
204
+ fig.add_trace(go.Scatter(x=[xt], y=[n_s], mode='markers+text', text=[f"{round(xt,1)}N"], textfont=dict(color="black", weight=800), textposition="top center", marker=dict(color=clr, size=12, symbol='x'), showlegend=False))
205
+
206
+ if view_mode != "Sec uniquement":
207
+ fig.add_trace(go.Scatter(x=x_grid, y=row['wet a']*x_grid + row['wet b'], name=f"{row['name']} (W)", line=dict(color=clr, width=2, dash='dot')))
208
 
209
+ # TA DEMANDE : VISUALISATION DES LIGNES DE NORMES EN NOIR
210
+ if n_s > 0 and view_mode != "Humide uniquement":
211
+ fig.add_hline(y=n_s, line_width=2, line_color="#000", annotation_text="NORME SEC", annotation_font=dict(color="black", weight=700))
212
+ if n_w > 0 and view_mode != "Sec uniquement":
213
+ fig.add_hline(y=n_w, line_width=2, line_dash="dot", line_color="#000", annotation_text="NORME HUMIDE", annotation_font=dict(color="black", weight=700))
214
 
215
+ fig.add_vline(x=effort_x, line_width=2, line_dash="dash", line_color="#555")
216
+
217
+ # FORÇAGE NOIR SUR TOUT LE GRAPHIQUE
218
+ fig.update_layout(
219
+ height=480, plot_bgcolor='white', paper_bgcolor='white', font=dict(color="black"),
220
+ xaxis=dict(title=dict(text="Effort Levier [N]", font=dict(color="black", weight=700)), tickfont=dict(color="black", weight=700), linecolor="black", gridcolor="#EEE"),
221
+ yaxis=dict(title=dict(text="Performance [N]", font=dict(color="black", weight=700)), tickfont=dict(color="black", weight=700), linecolor="black", gridcolor="#EEE"),
222
+ legend=dict(font=dict(color="black", weight=700), bordercolor="black", borderwidth=1, bgcolor="white")
223
+ )
224
  st.plotly_chart(fig, use_container_width=True)
225
 
226
+ # ==============================================================================
227
+ # 7. DASHBOARD ANALYSE (ALIGNE SUR TON EXEMPLE)
228
+ # ==============================================================================
229
+ st.markdown(f"**📊 Dynamics Analysis | {v_kmh}km/h | Energy: {int(e_total_j)} J**")
230
+ cols = st.columns(len(results_summary))
231
 
232
+ for i, res in enumerate(results_summary):
233
  with cols[i]:
234
+ is_bench = (res['name'] == ref_model_name)
235
+ st.markdown(f"<u>**{res['name']}**</u> {'⭐' if is_bench else ''}", unsafe_allow_html=True)
236
 
237
+ if view_mode != "Humide uniquement":
238
+ st.metric("Force Sec", f"{round(res['dry'], 1)} N", f"{round(res['dry'] - ref_f_sec, 1)} N vs Ref" if not is_bench else None)
239
+ d_sec = e_total_j / res['dry'] if res['dry'] > 0 else 0
240
+ st.write(f"🛑 **Arrêt (Sec): {round(d_sec, 2)} m**")
241
+ if n_s > 0:
242
+ xt = (n_s - res['row']['dry b']) / res['row']['dry a']
243
+ if xt > 180: st.markdown(f"<div class='alert-red'>HORS NORME SEC ({round(xt,1)}N)</div>", unsafe_allow_html=True)
 
 
 
244
  else: st.markdown(f"<div class='check-green'>CONFORME SEC</div>", unsafe_allow_html=True)
245
+
246
+ if view_mode != "Sec uniquement":
247
+ st.metric("Force Wet", f"{round(res['wet'], 1)} N", f"{round(res['wet'] - ref_f_wet, 1)} N vs Ref" if not is_bench else None)
248
+ d_wet = e_total_j / res['wet'] if res['wet'] > 0 else 0
249
+ st.write(f"🌧️ **Arrêt (Wet): {round(d_wet, 2)} m**")
250
+ if n_w > 0:
251
+ xtw = (n_w - res['row']['wet b']) / res['row']['wet a']
252
+ if xtw > 180: st.markdown(f"<div class='alert-red'>HORS NORME HUMIDE</div>", unsafe_allow_html=True)
 
 
253
  else: st.markdown(f"<div class='check-green'>CONFORME HUMIDE</div>", unsafe_allow_html=True)
254
 
255
+ if show_loss and view_mode == "Sec & Humide":
256
+ loss = ((res['dry'] - res['wet']) / res['dry'] * 100) if res['dry'] != 0 else 0
257
+ st.metric("Wet Loss", f"-{round(loss, 1)}%", delta_color="inverse")
258
+
259
  except Exception as e:
260
+ st.error(f"Erreur Système : {e}")