🎯 But pédagogique¶
Illustrer comment transformer des échantillons de longueurs variables en composites de longueur fixe, tout en respectant un seuil minimal de couverture.
Cela permet d’uniformiser les données avant les analyses statistiques ou géostatistiques.
💡 Note : Le compositing consiste à diviser un trou de forage en intervalles de même longueur (ex. 3 m), et à calculer la teneur moyenne pondérée dans chaque intervalle.
Si l’intervalle est partiellement couvert par les échantillons, une vérification du taux de couverture est effectuée avant d’accepter ou non le composite.
⚙️ Fonctionnalités interactives¶
Ajout ou suppression d’échantillons avec :
- Profondeur de début (De),
- Profondeur de fin (À),
- Valeur (teneur),
- Nom de l’échantillon.
Paramètres de compositing personnalisables :
- Longueur du composite (ex. 3 m),
- Seuil de couverture minimum (ex. 50 %).
Validation automatique :
- Vérification que les échantillons ne se chevauchent pas,
- Rejet des composites insuffisamment couverts.
Source
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import ipywidgets as widgets
from IPython.display import display, clear_output
sample_widgets = []
output = widgets.Output()
initial_samples = [
{'from': 0, 'to': 1, 'value': 1.00, 'label': 'Échantillon 1'},
{'from': 2, 'to': 3, 'value': 5.85, 'label': 'Échantillon 2'},
{'from': 4, 'to': 6, 'value': 1.75, 'label': 'Échantillon 3'}
]
def create_sample_widget(index, sample=None):
from_input = widgets.BoundedFloatText(value=sample['from'] if sample else 0.0, min=0, max=9999, step=0.1, description='De:', layout=widgets.Layout(width='120px'))
to_input = widgets.BoundedFloatText(value=sample['to'] if sample else 1.0, min=0, max=9999, step=0.1, description='À:', layout=widgets.Layout(width='120px'))
value_input = widgets.BoundedFloatText(value=sample['value'] if sample else 0.0, min=0, max=100, step=0.1, description='Teneur:', layout=widgets.Layout(width='120px'))
label_input = widgets.Text(value=sample['label'] if sample else f'Échantillon {index+1}', description='Nom:', layout=widgets.Layout(width='200px'))
delete_button = widgets.Button(description='Supprimer', button_style='danger', layout=widgets.Layout(width='100px'))
row = widgets.HBox([from_input, to_input, value_input, label_input, delete_button])
def on_delete_clicked(b):
sample_widgets.remove((from_input, to_input, value_input, label_input, row))
sample_box.children = [w[-1] for w in sample_widgets]
delete_button.on_click(on_delete_clicked)
sample_widgets.append((from_input, to_input, value_input, label_input, row))
return row
def draw_chart(samples, composites):
fig, ax = plt.subplots(figsize=(10, 2.5))
ax.set_ylim(-2, 2)
min_from = min([s['from'] for s in samples] + [c['from'] for c in composites])
max_to = max([s['to'] for s in samples] + [c['to'] for c in composites])
ax.set_xlim(min_from, max_to)
for s in samples:
rect = patches.Rectangle((s['from'], 0.5), s['to'] - s['from'], 0.9, facecolor='skyblue', edgecolor='black')
ax.add_patch(rect)
ax.text((s['from'] + s['to'])/2, 0.95, f"{s['value']:.2f}%", ha='center', va='center', fontsize=9)
ax.text((s['from'] + s['to'])/2, 1.5, s['label'], ha='center', va='bottom', fontsize=9)
ax.plot([s['from'], s['from']], [0.5, 1.4], color='black', linewidth=1)
if samples:
ax.plot([samples[-1]['to'], samples[-1]['to']], [0.5, 1.4], color='black', linewidth=1)
for c in composites:
rect = patches.Rectangle(
(c['from'], -1.4),
c['to'] - c['from'],
0.9,
facecolor='lightgreen' if c.get('covered', True) else 'lightgrey',
edgecolor='black'
)
ax.add_patch(rect)
if c.get('covered', True):
ax.text((c['from'] + c['to'])/2, -0.95, f"{c['value']:.2f}%", ha='center', va='center', fontsize=9)
ax.text((c['from'] + c['to'])/2, -2, c['label'], ha='center', va='top', fontsize=9)
ax.plot([c['from'], c['from']], [-1.4, -0.5], color='black', linewidth=1)
if composites:
ax.plot([composites[-1]['to'], composites[-1]['to']], [-1.4, -0.5], color='black', linewidth=1)
ax.set_xlabel("Profondeur (m)")
ax.set_yticks([])
ax.tick_params(bottom=True, labelbottom=True)
plt.tight_layout()
plt.show()
def make_composites(samples, comp_length, min_coverage_ratio):
if not samples:
return []
min_depth = min(s['from'] for s in samples)
max_depth = max(s['to'] for s in samples)
composites = []
start = min_depth
while start < max_depth:
end = start + comp_length
total_length = 0
weighted_sum = 0
for s in samples:
overlap_start = max(start, s['from'])
overlap_end = min(end, s['to'])
length = overlap_end - overlap_start
if length > 0:
weighted_sum += s['value'] * length
total_length += length
coverage_ratio = total_length / comp_length
if coverage_ratio >= min_coverage_ratio:
comp_value = weighted_sum / total_length
covered = True
else:
comp_value = 0
covered = False
composites.append({
'from': start,
'to': end,
'value': comp_value,
'label': f"Composite {len(composites)+1}",
'covered': covered
})
start = end
return composites
def on_calculate_clicked(b):
with output:
clear_output()
samples = []
for from_input, to_input, value_input, label_input, _ in sample_widgets:
try:
f = float(from_input.value)
t = float(to_input.value)
v = float(value_input.value)
label = label_input.value
if f >= t:
print(f"❌ Erreur : 'De' doit être inférieur à 'À' pour {label}")
return
samples.append({'from': f, 'to': t, 'value': v, 'label': label})
except:
print("❌ Erreur dans les valeurs d’un échantillon.")
return
# Vérification des chevauchements
samples_sorted = sorted(samples, key=lambda s: s['from'])
for i in range(len(samples_sorted) - 1):
current = samples_sorted[i]
next_sample = samples_sorted[i + 1]
if current['to'] > next_sample['from']:
print(f"❌ Erreur : Les échantillons « {current['label']} » et « {next_sample['label']} » se chevauchent.")
print("Veuillez corriger les chevauchements avant de continuer.")
return
try:
comp_length = float(composite_length_input.value)
if comp_length <= 0:
print("❌ Longueur de composite invalide.")
return
except:
print("❌ Erreur dans la longueur de composite.")
return
try:
min_coverage = float(min_coverage_input.value) / 100
if not (0 <= min_coverage <= 1):
print("❌ Le seuil de couverture doit être entre 0 et 100.")
return
except:
print("❌ Erreur dans le seuil de couverture.")
return
composites = make_composites(samples, comp_length, min_coverage)
draw_chart(samples, composites)
def on_add_sample_clicked(b):
new_widget = create_sample_widget(len(sample_widgets))
sample_box.children += (new_widget,)
# Widgets
composite_length_input = widgets.BoundedFloatText(
value=3.0, min=0.1, max=100, step=0.1,
description='Longueur composite:',
layout=widgets.Layout(width='180px'),
style={'description_width': 'initial'}
)
min_coverage_input = widgets.BoundedFloatText(
value=50.0, min=0, max=100, step=1,
description='Couverture min (%):',
layout=widgets.Layout(width='180px'),
style={'description_width': 'initial'}
)
add_sample_button = widgets.Button(description="Ajouter un échantillon", button_style='info')
add_sample_button.on_click(on_add_sample_clicked)
calculate_button = widgets.Button(description="Calculer et afficher", button_style='success')
calculate_button.on_click(on_calculate_clicked)
sample_box = widgets.VBox()
# Ajouter les exemples initiaux
for i, s in enumerate(initial_samples):
sample_box.children += (create_sample_widget(i, s),)
# Affichage
display(widgets.HTML("<h3>Échantillons initiaux (modifiables et supprimables) :</h3>"))
display(add_sample_button, sample_box)
display(widgets.HTML("<h3>Paramètres de calcul :</h3>"))
display(widgets.HBox([composite_length_input, min_coverage_input]))
display(calculate_button, output)
Loading...