Skip to article frontmatterSkip to article content

🎯 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...