Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

🧪 Atelier 2 – Exploration de la nuée variographique

🎯 Objectif général

Comprendre comment chaque paire de points d’échantillonnage contribue à la nuée variographique, et observer l’effet du nombre de points sur la qualité du variogramme obtenu.


🧠 Ce que vous allez apprendre

  • Comment se forment les nuages de demi-variances selon la distance.

  • Comment on passe d’une nuée à un variogramme expérimental.

  • Pourquoi un petit nombre d’échantillons peut fausser l’interprétation (variabilité, instabilité).


⚙️ Ce que vous pouvez faire

  • Générer un champ spatial simulé.

  • Choisir un nombre d’échantillons à prélever.

  • Voir toutes les paires de points et leur contribution au variogramme.

  • Suivre pas à pas la construction d’un variogramme :

    • Distance entre deux points

    • Valeurs mesurées

    • Calcul de la demi-variance


Source
import numpy as np
import matplotlib.pyplot as plt
from scipy.fft import fft2, ifft2
from scipy.spatial.distance import pdist
from ipywidgets import IntText, FloatText, VBox, Button, Output, Tab
import matplotlib.gridspec as gridspec
from IPython.display import display, clear_output

class VariogramWorkshop:
    def __init__(self):
        # Widgets paramètres (recalcul)
        self.n_samples_widget = IntText(value=100, description="Nb échantillons")
        self.range_widget = FloatText(value=30.0, description="Portée")
        self.sill_widget = FloatText(value=1.0, description="Sill")
        self.nugget_widget = FloatText(value=0.1, description="Nugget")
        self.bin_width_widget = FloatText(value=10.0, description="Bin (m)")
        self.calc_button = Button(description="Calculer", button_style='success')

        # Bouton suivant
        self.next_button = Button(description="Suivant", button_style='info')
        self.next_button.disabled = True

        # Outputs
        self.out_plot = Output()
        self.out_text = Output()

        # Onglets
        self.tabs = Tab(children=[self.out_plot, self.out_text])
        self.tabs.set_title(0, "Visualisation")
        self.tabs.set_title(1, "Calculs")

        # Data placeholders
        self.sample_coords = None
        self.sample_values = None
        self.dists = None
        self.gamma = None
        self.n_samples = 0
        self.field = None
        self.pair_index = 0

        # Connexions
        self.calc_button.on_click(self.on_calculate)
        self.next_button.on_click(self.on_next_pair)

        # Affichage interface
        param_box = VBox([
            self.n_samples_widget, self.range_widget, self.sill_widget,
            self.nugget_widget, self.bin_width_widget, self.calc_button])
        full_ui = VBox([param_box, self.next_button, self.tabs])
        display(full_ui)

    def generate_gaussian_field_fftma(self, size, range_, sill, nugget, seed=0):
        np.random.seed(seed)
        extended_size = (2 * size[0], 2 * size[1])
        x = np.arange(extended_size[0])
        y = np.arange(extended_size[1])
        X, Y = np.meshgrid(x, y, indexing='ij')
        h = np.sqrt((X - size[0])**2 + (Y - size[1])**2)
        cov = sill * np.exp(-h / range_)
        cov[0, 0] += nugget
        spectrum = np.abs(fft2(cov))**0.5
        white_noise = np.random.normal(size=extended_size)
        field = np.real(ifft2(fft2(white_noise) * spectrum))
        field_cropped = field[:size[0], :size[1]]
        std = np.std(field_cropped)
        return field_cropped / std * np.sqrt(sill+nugget) if std > 0 else field_cropped

    def pdist_to_indices(self, k, n):
        i = n - 2 - int(np.floor(np.sqrt(-8*k + 4*n*(n-1)-7)/2.0 - 0.5))
        j = k + i + 1 - n*(n-1)//2 + (n - i)*((n - i) -1)//2
        return i, j

    def on_calculate(self, b):
        # Récupération des paramètres de l'interface
        n_samples = self.n_samples_widget.value
        range_ = self.range_widget.value
        sill = self.sill_widget.value
        nugget = self.nugget_widget.value
        bin_width = self.bin_width_widget.value

        # Taille de la grille (200x200 ici)
        size = (200, 200)

        # Génération d'un nouveau champ gaussien
        self.field = self.generate_gaussian_field_fftma(size, range_, sill, nugget, seed=12345)

        # Sélection aléatoire des échantillons à partir de la grille
        sample_indices = np.random.choice(size[0]*size[1], n_samples, replace=False)
        self.sample_coords = np.array(np.unravel_index(sample_indices, size)).T
        self.sample_values = self.field[self.sample_coords[:, 0], self.sample_coords[:, 1]]
        self.n_samples = n_samples

        # Calcul des distances entre les échantillons
        self.dists = pdist(self.sample_coords)
    
        # Calcul des différences et de la demi-variance (γ)
        diffs = pdist(self.sample_values[:, None])
        self.gamma = 0.5 * diffs**2

        # Calcul du nombre maximal de paires d'échantillons
        self.max_pairs = n_samples * (n_samples - 1) // 2
        self.pair_index = 0

        # Déverrouiller le bouton suivant
        self.next_button.disabled = False

        # Mettre à jour les variables internes
        self.range_ = range_
        self.sill = sill
        self.nugget = nugget
        self.bin_width = bin_width
        self.size = size

        # Mettre à jour l'affichage du graphique
        self.update_plot()


    def update_plot(self):
        pair_index = self.pair_index
        n_samples = self.n_samples

        i, j = self.pdist_to_indices(pair_index, n_samples)
        p1 = self.sample_coords[i]
        p2 = self.sample_coords[j]
        z1 = self.sample_values[i]
        z2 = self.sample_values[j]
        dist_p = self.dists[pair_index]
        gamma_p = self.gamma[pair_index]

        # Binning variogramme expérimental
        max_dist = 200
        bins = np.arange(0, max_dist + self.bin_width, self.bin_width)
        bin_centers = 0.5 * (bins[:-1] + bins[1:])
        bin_indices = np.digitize(self.dists, bins) - 1
        experimental = [np.mean(self.gamma[bin_indices == k]) if np.any(bin_indices == k) else np.nan for k in range(len(bin_centers))]

        # Modèle théorique
        h_vals = np.linspace(0, max_dist, 200)
        theoretical = self.nugget + self.sill * (1 - np.exp(-h_vals / self.range_))

        # Affichage graphique
        with self.out_plot:
            clear_output(wait=True)
            fig = plt.figure(figsize=(14, 5))
            gs = gridspec.GridSpec(1, 2, width_ratios=[1, 1.2])

            ax0 = fig.add_subplot(gs[0])
            ax0.imshow(self.field, cmap='viridis', origin='lower')
            ax0.scatter(self.sample_coords[:, 1], self.sample_coords[:, 0], c='r', s=10, label='Échantillons')
            ax0.scatter([p1[1], p2[1]], [p1[0], p2[0]], c='yellow', s=50, edgecolors='black', label='Points choisis')
            ax0.set_title("Champ Gaussien 2D")
            ax0.legend(loc='upper right')

            ax1 = fig.add_subplot(gs[1])
            ax1.scatter(self.dists, self.gamma, alpha=0.3, label='Nuée variographique', color='cornflowerblue')
            ax1.plot(h_vals, theoretical, 'r-', label='Modèle théorique')
            ax1.plot(bin_centers, experimental, 'ko-', label='Variogramme expérimental')
            for b in bins:
                ax1.axvline(b, color='gray', linestyle='--', linewidth=0.5, alpha=0.5)
            ax1.scatter(dist_p, gamma_p, c='red', s=100, label='Couple choisi')
            ax1.set_xlim(0, 200)
            ax1.set_xlabel("Distance h (m)")
            ax1.set_ylabel("½(zᵢ - zⱼ)²")
            ax1.set_title("Nuée variographique")
            ax1.legend(loc='upper right')

            plt.tight_layout()
            plt.show()

        # Affichage texte dans l'onglet dédié
        with self.out_text:
            clear_output(wait=True)
            txt = (
                f"Point 1 (y,x): {p1}, valeur z₁ = {z1:.3f}\n"
                f"Point 2 (y,x): {p2}, valeur z₂ = {z2:.3f}\n"
                f"Distance = ||p₁ - p₂|| = {dist_p:.2f}\n"
                f"Calcul demi-variance : γ(h) = ½ (z₁ - z₂)² = ½ * ({z1:.3f} - {z2:.3f})² = {gamma_p:.3f}"
            )
            print(txt)

    def on_next_pair(self, b):
        self.pair_index += 1
        if self.pair_index >= self.max_pairs:
            self.pair_index = 0
        self.update_plot()

# Lancer l'interface
VariogramWorkshop()
Loading...
<__main__.VariogramWorkshop at 0x2120f0c7b50>