Skip to article frontmatterSkip to article content

🎯 But pédagogique

Comprendre intuitivement comment ajuster un modèle théorique à un variogramme expérimental avec présence d’anisotrope (2D).

⚙️ Fonctionnalités

  • Choisir les structures
  • Sélection des types de structures :
    • Effet de pépite
    • Sphérique
    • Gaussien
    • Exponentiel
  • Sliders pour :
    • Variances (c0c_0 eet c1c_1)
    • Portée (apa_p et aga_g)
    • Angle (thetagtheta_g)
    • Type (nature)
Source
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, FloatSlider, IntSlider, Dropdown
import matplotlib.cm as cm

# --- FONCTIONS DE BASE ---

def anisotropic_covariance(hx, hy,
                           ranges_angles_models_weights,
                           c0=0.0):
    """
    Calcule une covariance imbriquée avec plusieurs structures anisotropes.

    ranges_angles_models_weights : liste de tuples (range_major, range_minor, angle_deg, model, c)
    c0 : effet nugget (valeur à h=0 uniquement)
    """
    cov_total = np.zeros_like(hx, dtype=float)

    for range_major, range_minor, angle_deg, model, c in ranges_angles_models_weights:
        angle_rad = np.deg2rad(angle_deg)
        cos_a, sin_a = np.cos(angle_rad), np.sin(angle_rad)
        h_rot = cos_a * hx + sin_a * hy
        v_rot = -sin_a * hx + cos_a * hy
        h_scaled = h_rot / range_major
        v_scaled = v_rot / range_minor
        h = np.sqrt(h_scaled**2 + v_scaled**2)

        if model == 'spherical':
            base_cov = np.where(h < 1, 1 - 1.5 * h + 0.5 * h**3, 0)
        elif model == 'exponential':
            base_cov = np.exp(-3 * h)
        elif model == 'gaussian':
            base_cov = np.exp(-3 * h**2)
        elif model == 'nugget':
            base_cov = np.where(h == 0, 1, 0)
        else:
            raise ValueError("Modèle inconnu : {}".format(model))

        cov_total += c * base_cov

    # Nugget uniquement au point h=0
    cov_total += c0 * (hx == 0) * (hy == 0)

    return cov_total


def fftma_2d_nested(nx, ny, nested_params, c0=0.0, seed=None):
    if seed is not None:
        np.random.seed(seed)
    nfft_x, nfft_y = 2 * nx, 2 * ny
    x = np.arange(-nx, nx)
    y = np.arange(-ny, ny)
    hx, hy = np.meshgrid(x, y, indexing='ij')

    cov = anisotropic_covariance(hx, hy, nested_params, c0)

    spectrum = np.fft.fft2(np.fft.ifftshift(cov))
    spectrum = np.real(spectrum)
    spectrum[spectrum < 0] = 0
    amp = np.sqrt(spectrum)
    noise = np.random.normal(0, 1, (nfft_x, nfft_y))
    noise_fft = np.fft.fft2(noise)
    field_fft = amp * noise_fft
    field = np.fft.ifft2(field_fft).real
    field = field[:nx, :ny]
    return field


def directional_variogram(field, n_samples=500, max_lag=20, lag_step=1, directions_deg=None, tol=10):
    nx, ny = field.shape
    X, Y = np.meshgrid(np.arange(nx), np.arange(ny), indexing='ij')
    indices = np.random.choice(nx * ny, size=n_samples, replace=False)
    coords = np.column_stack((X.ravel()[indices], Y.ravel()[indices]))
    values = field.ravel()[indices]
    h_vectors = coords[:, None, :] - coords[None, :, :]
    h_dist = np.linalg.norm(h_vectors, axis=2)
    h_angle = np.arctan2(h_vectors[..., 1], h_vectors[..., 0]) * 180 / np.pi
    h_angle = (h_angle + 180) % 180
    gamma_by_dir = {}
    if directions_deg is None:
        directions_deg = np.arange(0, 180, 22.5)
    lag_bins = np.arange(0, max_lag + lag_step, lag_step)
    lag_centers = 0.5 * (lag_bins[:-1] + lag_bins[1:])
    for theta in directions_deg:
        mask_dir = (np.abs(h_angle - theta) <= tol)
        gammas = []
        for k in range(len(lag_bins) - 1):
            mask_lag = (h_dist >= lag_bins[k]) & (h_dist < lag_bins[k + 1])
            mask = mask_dir & mask_lag
            if not np.any(mask):
                gammas.append(np.nan)
                continue
            i, j = np.where(mask)
            gamma = 0.5 * np.mean((values[i] - values[j])**2)
            gammas.append(gamma)
        gamma_by_dir[theta] = gammas
    return gamma_by_dir, lag_centers, coords, values, X, Y, field

# --- SIMULATION FIXE ---

nx, ny = 250, 250
seed = 42

# Deux structures imbriquées + nugget
nested_params = [
    (80, 20, 22.5, 'spherical', 0.7),   # Structure large orientée N-S
    (1, 1, 1, 'nugget', 0.3)     # Structure fine orientée E-W
]

field = fftma_2d_nested(nx, ny, nested_params, c0=0.1, seed=42)

# --- WIDGET D'AJUSTEMENT ---
def update_model(n_samples, lag_step, c0, c1, model, range_major, range_minor, angle_deg):

    gamma_exp, lags, coords, values, X, Y, _ = directional_variogram(field, n_samples=n_samples, max_lag=50, lag_step=lag_step, tol=10)

    directions = list(gamma_exp.keys())
    fig, axs = plt.subplots(3, 3, figsize=(10, 8))
    for i in range(8):
        angle = directions[i]
        ax = axs[i // 3, i % 3]
        ax.plot(lags, gamma_exp[angle], 'o-', label='Expérimental', color='tab:blue')
        # Courbe théorique
        h = lags / range_major  # Approximation isotrope sur direction principale
        if model == 'spherical':
            gamma_th = np.where(h < 1, c0 + c1 * (1.5 * h - 0.5 * h**3), c0 + c1)
        elif model == 'exponential':
            gamma_th = c0 + c1 * (1 - np.exp(-3 * h))
        elif model == 'gaussian':
            gamma_th = c0 + c1 * (1 - np.exp(-3 * h**2))
        ax.plot(lags, gamma_th, '-', color='black', label='Théorique')
        ax.set_title(f'Direction {angle}°')
        ax.set_xlabel('Lag')
        ax.set_ylabel('γ(h)')
        ax.set_ylim(0, 1.8)
        ax.set_xlim(0, np.max(lags))
        ax.grid(True)
    axs[2, 2].axis('off')
    plt.tight_layout()
    plt.show()
    
    # Figure 2 : champ simulé + points échantillonnés
    fig2, ax = plt.subplots(figsize=(6, 4))
    im = ax.imshow(field, cmap='jet', origin='lower', alpha=0.4)
    ax.set_title('Champ simulé avec points échantillonnés')

    nx_grid, ny_grid = field.shape
    X_grid, Y_grid = np.meshgrid(np.arange(nx_grid), np.arange(ny_grid), indexing='ij')

    sampled_indices = coords[:,0]*ny_grid + coords[:,1]
    
    mask_all = np.ones(nx_grid * ny_grid, dtype=bool)
    mask_all[sampled_indices] = False

    X_all = X_grid.ravel()[mask_all]
    Y_all = Y_grid.ravel()[mask_all]
    vals_all = field.ravel()[mask_all]

    X_samp = coords[:,0]
    Y_samp = coords[:,1]
    vals_samp = values

    norm = plt.Normalize(vmin=np.min(field), vmax=np.max(field))
    cmap = cm.jet
    colors = cmap(norm(vals_samp))
    ax.scatter(Y_samp, X_samp, s=80, facecolors='none', edgecolors='black', linewidth=1.2, label='Échantillonnés')
    sc = ax.scatter(Y_samp, X_samp, s=60, c=vals_samp, cmap='jet', norm=norm, label='Valeur')

    plt.colorbar(sc, ax=ax, fraction=0.046, pad=0.04)
    ax.set_xlim(0, nx-1)
    ax.set_ylim(0, ny-1)

    plt.tight_layout()
    plt.show()

# Créer le widget interactif
interact(update_model,
         range_major=FloatSlider(min=5, max=50, step=1, value=20, description='Range major'),
         range_minor=FloatSlider(min=2, max=50, step=1, value=10, description='Range minor'),
         angle_deg=IntSlider(min=0, max=180, step=5, value=45, description='Angle (°)'),
         model=Dropdown(options=['spherical', 'exponential', 'gaussian'], value='spherical', description='Model'),
         c0=FloatSlider(min=0, max=1, step=0.01, value=0, description='Nugget c0'),
         c1=FloatSlider(min=0, max=2, step=0.01, value=1, description='Sill c1'),
         n_samples=IntSlider(min=50, max=2000, step=25, value=100, description='n_samples'),
         lag_step=IntSlider(min=1, max=20, step=1, value=5, description='Lag'))

Loading...