Ce notebook permet de visualiser les lignes d’isocontours de l’écart-type relatif de Gy.
Vous pouvez entrer vos propres paramètres, les valider, et obtenir une procédure graphique adaptée à vos étapes.
✅ Étapes :¶
Entrer vos paramètres globaux :
: proportion massique du lot analysé
, : masse spécifique du constituant d’intérêt et masse spécifique de la gangue
: taille à laquelle le constituant d’intérêt est entièrement libéré (en cm)
: masse totale du lot (en g)
s_r : écart-type relatif souhaité (ex.
1%)
Ajouter une ou plusieurs étapes avec leurs paramètres spécifiques :
: masse d’échantillon (en g)
: masse totale à l’étape (en g)
: taille max des fragments (en cm)
Visualiser l’abaque mis à jour avec :
les lignes d’isocontours en gris pour les écarts-types choisis
la ligne rouge représente l’écart-type relatif souhaité
les flèches bleues indiquant la progression entre les étapes d’échantillonnage et de concassage/pulvérisation
le calcul et l’affichage de l’écart-type relatif global ( global)
📊 Exemple de sortie :¶
L’abaque affiche une échelle logarithmique en abscisse (taille des fragments, cm) et en ordonnée (masse de l’échantillon, g).
Les courbes grises sont les isocontours pour les écarts-types relatifs choisis, tandis que les courbes en gris pointillé correspondent au maillage logarithmique.
Chaque point bleu correspond à une étape saisie, avec sa valeur affichée en rouge à côté.
Les flèches bleues montrent le cheminement entre étapes.
Enfin, le global (écart-type total combiné) est indiqué dans le titre et sorti en texte.
Remarque : les calculs reposent sur des paramètres et formules spécifiques.
Assurez-vous de bien ajuster les valeurs selon votre contexte d’étude.
Source
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
from ipywidgets import VBox, HBox, Button, FloatText, Output, Label, Layout, GridBox, HTML
from IPython.display import display, clear_output, Math
# --- Paramètres globaux ---
params = {
'al': 0.010 / 0.67,
'da': 4.1,
'dg': 2.8,
'd0': 0.1
}
common_layout = Layout(width='200px')
common_style = {'description_width': 'initial'}
# Widgets paramètres globaux
al_input = FloatText(description='$a_L$ (conc.)', value=params['al'], step=0.001, layout=common_layout, style=common_style, format='.4f')
da_input = FloatText(description='$\delta_a$ (densité min.)', value=params['da'], step=0.1, layout=common_layout, style=common_style)
dg_input = FloatText(description='$\delta_g$ (densité gangue)', value=params['dg'], step=0.1, layout=common_layout, style=common_style)
d0_input = FloatText(description='$d_0$ (taille lib.)', value=params['d0'], step=0.0001, layout=common_layout, style=common_style, format='.4f')
params_widgets = VBox([al_input, da_input, dg_input, d0_input])
sr_desire_input = FloatText(value=10.0, step=0.1, layout=Layout(width='100px'))
validation_label = Label()
error_label = Label(value='', layout=Layout(margin='10px 0 0 0', color='red'))
# --- Fonction Gy simplifiée (modifiée pour accepter les arrays NumPy) ---
def gy(al, da, dg, ml, d0, me, d, f=0.5, g=0.25):
# Gestion des entrées scalaires ou vectorielles
d = np.asarray(d)
me = np.asarray(me)
fl = np.minimum(np.sqrt(d0 / d), 1.0)
ud = (1 - al) / al * ((1 - al) * da + al * dg)
k = ud * f * g * fl
with np.errstate(divide='ignore', invalid='ignore'):
s2 = k * d**3 / me * (1 - me / ml)
# CORRECTION : Gérer différemment les scalaires et les tableaux
if np.isscalar(s2):
# Si c'est un simple nombre, utiliser une condition simple
if s2 < 0:
s2 = np.nan
else:
# Si c'est un tableau, utiliser l'indexation pour modifier les valeurs
s2[s2 < 0] = np.nan
sr = np.sqrt(s2)
return sr
# --- Étapes et interface ---
steps = []
gridbox_container = VBox()
output = Output()
def refresh_gridbox():
all_widgets = []
# Création des en-têtes pour le tableau
headers = [Label(value=h, layout=Layout(font_weight='bold')) for h in ["Masse Échantillon (g)", "Masse Lot (g)", "Taille Particules (cm)"]]
all_widgets.extend(headers)
for me_box, ml_box, d_box in steps:
all_widgets.extend([me_box, ml_box, d_box])
global steps_grid
steps_grid = GridBox(all_widgets, layout=Layout(grid_template_columns="160px 160px 160px", grid_gap="5px"))
gridbox_container.children = [steps_grid]
def add_step(_=None):
me_input = FloatText(value=100.0, step=1.0, layout=Layout(width='100px'))
# La masse du lot de la nouvelle étape est par défaut la masse de l'échantillon précédent
prev_me = steps[-1][0].children[1].value if steps else 1000.0
ml_input = FloatText(value=prev_me, step=1.0, layout=Layout(width='100px'))
d_input = FloatText(value=0.1, step=0.01, layout=Layout(width='100px'))
me_box = HBox([Label(value="Mₑ", layout=Layout(width='25px')), me_input])
ml_box = HBox([Label(value="Mᴸ", layout=Layout(width='25px')), ml_input])
d_box = HBox([Label(value="d", layout=Layout(width='25px')), d_input])
steps.append((me_box, ml_box, d_box))
refresh_gridbox()
def remove_step(_=None):
if steps:
steps.pop()
refresh_gridbox()
def update_params(_=None):
try:
params['al'] = float(al_input.value)
params['da'] = float(da_input.value)
params['dg'] = float(dg_input.value)
params['d0'] = float(d0_input.value)
error_label.value = ''
except Exception as e:
error_label.value = f"Erreur de saisie dans les paramètres globaux: {e}"
def update_plot(_=None):
with output:
clear_output(wait=True)
update_params()
sr_list, d_list, me_list, ml_list = [], [], [], []
# Récupération des étapes
for me_box, ml_box, d_box in steps:
try:
me = float(me_box.children[1].value)
ml = float(ml_box.children[1].value)
d = float(d_box.children[1].value)
if me <= 0 or ml <= 0 or d <= 0 or me >= ml:
display(Math(r"Erreur : M_e > 0,\; M_L > 0,\; d > 0 \text{ et } M_e < M_L. \text{ Étape ignorée.}"))
continue
sr = gy(params['al'], params['da'], params['dg'], ml,
params['d0'], me, d)
sr_list.append(sr)
d_list.append(d)
me_list.append(me)
ml_list.append(ml)
except Exception as e:
error_label.value = f"Erreur de saisie dans les étapes: {e}"
return
if not sr_list:
print("Aucune étape valide à calculer. Vérifiez les valeurs.")
return
# --- Affichage des étapes pour debug ---
print("Étapes valides récupérées :")
for i, (me, ml, d, sr) in enumerate(zip(me_list, ml_list, d_list, sr_list), 1):
display(Math(r"\text{Étape %d: } M_e = %g,\, M_L = %g,\, d = %g,\, s_r = %.3f\%%" % (i, me, ml, d, sr*100)))
sr_global = np.sqrt(np.sum(np.array(sr_list) ** 2))
sr_desire = sr_desire_input.value / 100.0
red_level_percent = sr_desire_input.value
fig, ax = plt.subplots(figsize=(10, 8))
# --- Grille fixe pour D et ME ---
me_min, me_max = min(me_list) * 0.1, max(ml_list) * 10
if me_min <= 0:
me_min = min(me_list) * 0.5
d_grid = np.logspace(-3, 1, 500) # grille D fixe 10^-3 → 10^1
me_grid = np.logspace(np.log10(me_min), np.log10(me_max*2), 500)
D, ME = np.meshgrid(d_grid, me_grid)
# Abaque de référence
SR_grid = gy(params['al'], params['da'], params['dg'],
ml_list[0], params['d0'], ME, D)
# Niveaux en %
levels_percent = [0.01, 0.05, 0.1, 0.5, 1, 5, 10, 50]
me_label_line = float(ml_list[0]) / 50 # ligne pour labels
# Tracer contours gris
contour_set = ax.contour(
D, ME, SR_grid * 100, levels=levels_percent,
colors='grey', linestyles='solid',
linewidths=0.8, alpha=0.8,
extend='both' # <-- ajout
)
# --- Placement manuel des labels horizontaux ---
for lvl, segs in zip(contour_set.levels, contour_set.allsegs):
placed = False
for seg in segs:
if seg.size == 0:
continue
x, y = seg[:, 0], seg[:, 1]
# vérifier si la courbe croise la ligne me_label_line
if np.min(y) <= me_label_line <= np.max(y):
idxs = np.where(np.diff(np.sign(y - me_label_line)) != 0)[0]
if idxs.size > 0:
i = idxs[0]
y1, y2 = y[i], y[i+1]
x1, x2 = x[i], x[i+1]
x_cross = x1 + (me_label_line - y1) / (y2 - y1) * (x2 - x1) if y2 != y1 else x1
else:
k = np.argmin(np.abs(y - me_label_line))
x_cross = x[k]
# label manuel horizontal
ax.text(x_cross, me_label_line, f"{lvl:.2f}%",
color='grey', fontsize=8,
ha='center', va='bottom',
bbox=dict(facecolor='white', edgecolor='none', alpha=0.7, pad=1))
placed = True
break # un label par niveau
# Ligne rouge cible
try:
ax.contour(D, ME, SR_grid * 100,
levels=[red_level_percent],
colors='red', linewidths=2, linestyles='--')
except Exception:
pass
# --- Tracé du chemin procédure ---
path_x, path_y = [], []
path_x.append(d_list[0])
path_y.append(ml_list[0])
for i in range(len(d_list)):
path_x.append(d_list[i])
path_y.append(me_list[i])
if i < len(d_list) - 1:
path_x.append(d_list[i+1])
path_y.append(me_list[i])
ax.plot(path_x, path_y, 'o-', color='blue',
markersize=8, label='Procédure Échantillonnage/Concassage')
# Annotations étapes
ax.text(path_x[0] * 1.05, path_y[0], 'Lot Initial',
color='blue', fontsize=9)
for i, sr in enumerate(sr_list):
idx_path = 2 * i + 1
if idx_path < len(path_x):
ax.text(path_x[idx_path] * 1.15, path_y[idx_path],
f'Étape {i+1}\nsr={sr*100:.3f}%',
color='darkgreen', fontsize=9)
ax.set_xlabel('Taille maximale des fragments d (cm)')
ax.set_ylabel("Masse de l'échantillon me (g)")
ax.set_xscale('log')
ax.set_yscale('log')
d_min_plot = 10**-3
d_max_plot = 10**1
ax.plot([d_min_plot, d_max_plot], [ml_list[0], ml_list[0]],
color='grey', linestyle='--', linewidth=1)
ax.set_xlim(d_min_plot, d_max_plot)
ax.set_ylim(me_min, me_max/3)
ax.grid(True, which='both', linestyle='--', linewidth=0.5)
ax.set_title(f'Abaque de Gy - sr_global = {sr_global*100:.3f}%')
ax.legend()
plt.show()
# Validation
if sr_global <= sr_desire:
validation_label.value = (
f"✅ Procédure valide (sr_global = {sr_global*100:.2f}% ≤ "
f"sr désiré = {sr_desire*100:.2f}%)"
)
else:
validation_label.value = (
f"❌ Procédure NON valide (sr_global = {sr_global*100:.2f}% > "
f"sr désiré = {sr_desire*100:.2f}%)"
)
# --- Boutons ---
btn_add = Button(description='Ajouter étape', button_style='success', icon='plus')
btn_add.on_click(add_step)
btn_remove = Button(description='Supprimer dernière', button_style='warning', icon='minus')
btn_remove.on_click(remove_step)
btn_calcul = Button(description='Calculer & Tracer', button_style='primary', icon='calculator')
btn_calcul.on_click(update_plot)
# --- Interface ---
controls = VBox([
Label("Paramètres fixes du matériau :"),
params_widgets,
HBox([Label("$s_r$ désiré (%) :"), sr_desire_input]),
HTML("<hr>"),
Label("Étapes de la procédure :"),
HBox([btn_add, btn_remove]),
gridbox_container,
HTML("<hr>"),
btn_calcul,
validation_label,
error_label
])
controls.layout = Layout(width='90%', padding='10px', border='1px solid #ccc')
output.layout = Layout(width='90%', padding='10px')
app_layout = VBox([controls, output])
# Initialisation avec une première étape
add_step()
display(app_layout)