Skip to article frontmatterSkip to article content

Ce graphique interactif illustre de façon concrète l’effet du changement d’échelle (support) sur la distribution des teneurs.

Au départ, deux gisements sont simulés avec une distribution statistique identique à l’échelle ponctuelle (1 m × 1 m).
Mais que se passe-t-il lorsqu’on augmente la taille des blocs ?

🔍 Grâce au bouton ci-dessous, vous pouvez ajuster la taille du bloc de support et observer en temps réel les conséquences :
Par exemple, un bloc de 10 m × 10 m est simplement la moyenne des 100 cellules de 1 m × 1 m qu’il contient.

🗺️ Les cartes, 📊 les histogrammes, et 📈 les fonctions de répartition cumulées sont mis à jour après activation du bouton calculer pour refléter les nouvelles propriétés statistiques du gisement après agrégation.

💡 C’est une façon simple mais puissante de visualiser l’effet de support — un concept clé en estimation minière.

🎯 Prenez quelques minutes pour explorer... et préparez vos questions pour la discussion en classe !

Source
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import VBox, HBox, IntText, Button, Output
from numpy.fft import fft2, ifft2, fftshift
from numpy.random import Generator, PCG64

# --- Configuration centralisée ---
# Regrouper les paramètres modifiables au même endroit facilite les ajustements.
CONFIG = {
    "size": 500,
    "range_short_factor": 10.0,
    "range_long_factor": 250.0,
    "sill": 1.0,
    "seed_short": 42,
    "seed_long": 24,
    "plot_vmin": 0,
    "plot_vmax": 10,
    "dtype": np.float32  # Utiliser float32 réduit l'usage mémoire de 50%
}

# --- Fonctions ---
def spherical_covariance_fft(size: int, range_val: float, sill: float = 1.0) -> np.ndarray:
    """
    Calcule la covariance sphérique sur une grille étendue pour la simulation FFT.

    Args:
        size (int): Taille de la grille de simulation finale.
        range_val (float): Portée du modèle de covariance.
        sill (float, optional): Le palier (variance). Défaut à 1.0.

    Returns:
        np.ndarray: Modèle de covariance centré via fftshift.
    """
    extended_size = 2 * size
    x = np.arange(-extended_size // 2, extended_size // 2)
    X, Y = np.meshgrid(x, x)
    h = np.sqrt(X**2 + Y**2).astype(CONFIG["dtype"])
    
    # Normalisation de la distance pour le calcul
    h_norm = h / range_val
    
    # Formule de la covariance sphérique
    cov = sill * (1 - 1.5 * h_norm + 0.5 * h_norm**3)
    
    # La covariance est nulle au-delà de la portée
    cov[h > range_val] = 0
    
    # fftshift centre la covariance pour la transformée de Fourier
    return fftshift(cov)

def fftma_simulation(
    size: int, range_val: float, rng: Generator, sill: float = 1.0
) -> np.ndarray:
    """
    Génère un champ gaussien 2D en utilisant la méthode FFT-MA.

    Args:
        size (int): Taille du champ à générer.
        range_val (float): Portée du variogramme.
        rng (Generator): Générateur de nombres aléatoires de NumPy.
        sill (float, optional): Le palier (variance). Défaut à 1.0.

    Returns:
        np.ndarray: Le champ gaussien simulé.
    """
    extended_size = 2 * size
    cov_model = spherical_covariance_fft(size, range_val, sill)
    
    # Utiliser np.abs() est une sécurité contre les imprécisions numériques
    cov_fft = np.sqrt(np.abs(fft2(cov_model)))
    
    # Génération du bruit blanc avec le dtype spécifié
    white_noise = rng.normal(size=(extended_size, extended_size)).astype(CONFIG["dtype"])
    white_fft = fft2(white_noise)
    
    # Convolution dans le domaine fréquentiel
    z_fft = cov_fft * white_fft
    z_ext = np.real(ifft2(z_fft))
    
    # Extraction de la partie centrale pour éviter les artefacts de bord
    start = extended_size // 4
    end = start + size
    return z_ext[start:end, start:end]

def gaussian_to_lognormal(field: np.ndarray) -> np.ndarray:
    """Convertit un champ gaussien en champ log-normal."""
    return np.exp(field)

def match_histogram(reference: np.ndarray, target: np.ndarray) -> np.ndarray:
    """Force l'histogramme du champ 'target' à correspondre à celui de 'reference'."""
    flat_ref = np.sort(reference.ravel())
    sorted_idx = np.argsort(target.ravel())
    result = np.zeros_like(target.ravel(), dtype=CONFIG["dtype"])
    result[sorted_idx] = flat_ref
    return result.reshape(target.shape)

def aggregate(field: np.ndarray, block_size: int) -> np.ndarray:
    """Agrége un champ par moyenne sur des blocs de taille block_size x block_size."""
    s = field.shape[0]
    # Assure que la taille du champ est un multiple de la taille du bloc
    trimmed_size = s - (s % block_size)
    trimmed = field[:trimmed_size, :trimmed_size]
    
    # Redimensionnement efficace pour calculer la moyenne par bloc
    reshaped = trimmed.reshape(
        trimmed_size // block_size, block_size,
        trimmed_size // block_size, block_size
    )
    return reshaped.mean(axis=(1, 3))

# --- Préparation des données initiales ---
# Utilisation du générateur de nombres aléatoires moderne de NumPy
rng_short = np.random.default_rng(CONFIG["seed_short"])
rng_long = np.random.default_rng(CONFIG["seed_long"])

size = CONFIG["size"]
range_short = CONFIG["range_short_factor"]
range_long = CONFIG["range_long_factor"]

gauss_short = fftma_simulation(size, range_short, rng_short, CONFIG["sill"])
gauss_long = fftma_simulation(size, range_long, rng_long, CONFIG["sill"])

lognorm_short = gaussian_to_lognormal(gauss_short)
lognorm_long = gaussian_to_lognormal(gauss_long)
lognorm_long = match_histogram(lognorm_short, lognorm_long)

# --- Affichage avec bouton ---
def plot_fields(support: int):
    """Affiche les champs, histogrammes et fonctions de répartition."""
    fig, axes = plt.subplots(2, 2, figsize=(8, 6), constrained_layout=True)
    
    agg_short = aggregate(lognorm_short, support)
    agg_long = aggregate(lognorm_long, support)
    
    # Dictionnaire pour éviter la répétition dans le traçage des images
    plot_data = {
        "short": {"data": agg_short, "ax": axes[0, 0], "title": "Portée courte"},
        "long":  {"data": agg_long, "ax": axes[0, 1], "title": "Portée longue"},
    }

    # Boucle pour tracer les deux cartes
    for key, val in plot_data.items():
        im = val["ax"].imshow(val["data"], cmap='viridis', 
                              vmin=CONFIG["plot_vmin"], vmax=CONFIG["plot_vmax"])
        val["ax"].set_title(f'{val["title"]} – Support {support}×{support}', fontsize=12)
        val["ax"].axis('off')
        fig.colorbar(im, ax=val["ax"])
    
    # Calculs pour les graphiques de distribution
    mean_val = agg_short.mean()
    bins = np.linspace(CONFIG["plot_vmin"], CONFIG["plot_vmax"], 11)

    # Histogrammes
    axes[1, 0].hist(agg_short.ravel(), bins=bins, alpha=0.7, color='steelblue', label='Portée courte', ec='black')
    axes[1, 0].hist(agg_long.ravel(), bins=bins, alpha=0.7, color='darkorange', label='Portée longue', ec='black')
    axes[1, 0].set_title('Histogrammes')

    # Fonctions de répartition empirique (ECDF)
    for data, color, label in zip([agg_short.ravel(), agg_long.ravel()],
                                  ['steelblue', 'darkorange'],
                                  ['Portée courte', 'Portée longue']):
        sorted_data = np.sort(data)
        ecdf = np.arange(1, len(sorted_data) + 1) / len(sorted_data)
        axes[1, 1].plot(sorted_data, ecdf, color=color, lw=2, label=label)
    axes[1, 1].set_title('Fonctions de répartition')

    # Paramètres communs aux deux graphiques du bas
    for ax in [axes[1, 0], axes[1, 1]]:
        ax.axvline(mean_val, color='black', linestyle='--', linewidth=2, label=f'Moyenne = {mean_val:.2f}')
        ax.set_xlim([CONFIG["plot_vmin"], CONFIG["plot_vmax"]])
        ax.grid(True)
        ax.legend()
    axes[1,1].set_ylim([0, 1])

    plt.show()

# --- Widgets ---
support_input = IntText(value=1, description="Support (m):", min=1)
calc_button = Button(description="Calculer", button_style="success")
output = Output()

def on_calculate_clicked(b):
    with output:
        output.clear_output(wait=True) # wait=True pour un affichage plus fluide
        support = support_input.value
        if support > 0:
            plot_fields(support)

calc_button.on_click(on_calculate_clicked)

# Affichage final
display(VBox([HBox([support_input, calc_button]), output]))
# Lancer un premier calcul pour l'affichage initial
on_calculate_clicked(None)
Loading...