Find Optimal Bootsize

In this Example we will define the optimal bootsize as the bootsize that minimizes errors between the true (no-bootstrap) Structure function (longitudinal,transverse,advective,velocity,scalar) and the bootstrapped Structure Function

Load Modules

[2]:
import xarray as xr
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm, SymLogNorm
import matplotlib.ticker as ticker
import pyturbo_sf as psf
from dataclasses import dataclass
from typing import List, Dict, Any, Optional, Tuple
import warnings
import pyproj
import numpy as np
from scipy import stats
import matplotlib.colors as mcolors
from matplotlib.pyplot import cm
linewidth = 2
fontsize = 12
plt.rcParams['xtick.labelsize'] = fontsize
plt.rcParams['ytick.labelsize'] = fontsize
plt.rcParams['xtick.major.width'] = 2
plt.rcParams['xtick.minor.width'] = 2
plt.rcParams['ytick.major.width'] = 2
plt.rcParams['ytick.minor.width'] = 2
plt.rcParams['xtick.major.size'] = 10
plt.rcParams['xtick.minor.size'] = 5
plt.rcParams['ytick.major.size'] = 10
plt.rcParams['ytick.minor.size'] = 5
plt.rcParams['savefig.dpi'] = 150
plt.rc('font', family='serif')
import gc

Find Optimal Bootsize for different Functions (advective,scalar,longitudinal,transverse,velocity)

[14]:
# =============================================================================
# CONFIGURATION
# =============================================================================

@dataclass
class SFConfig:
    """Structure function configuration."""
    name: str
    fun: str
    order: int
    variable_names: List[str]

    def __str__(self):
        return f"{self.name} ({self.fun}, order={self.order})"


# Define all configurations to test
SF_CONFIGS = [
    SFConfig(
        name="Longitudinal velocity SF2",
        fun='longitudinal',
        order=2,
        variable_names=['u', 'v']
    ),
    SFConfig(
        name="Transverse velocity SF2",
        fun='transverse',
        order=2,
        variable_names=['u', 'v']
    ),
    SFConfig(
        name="Default velocity SF2",
        fun='default_vel',
        order=2,
        variable_names=['u', 'v']
    ),
    SFConfig(
        name="Scalar SF1",
        fun='scalar',
        order=1,
        variable_names=['E']
    ),
    SFConfig(
        name="Advective (E gradients)",
        fun='advective',
        order=1,
        variable_names=['u', 'v', 'w', 'E_dx', 'E_dy', 'E_dz']
    ),
    SFConfig(
        name="Advective (pressure)",
        fun='advective',
        order=1,
        variable_names=['u', 'v', 'dpdx', 'dpdy']
    ),
    SFConfig(
        name="Advective (stress)",
        fun='advective',
        order=1,
        variable_names=['u', 'v', 'tau_x', 'tau_y']
    ),
]


# =============================================================================
# CORE FUNCTIONS
# =============================================================================

def generate_bootsizes(base_bootsize_x: int, base_bootsize_y: int,
                       min_power: int = 2) -> Tuple[np.ndarray, np.ndarray]:
    """Generate bootsizes by dividing base sizes by powers of 2."""
    max_power = int(np.log2(min(base_bootsize_x, base_bootsize_y)))
    powers = np.arange(min_power, max_power)
    bootsizes_x = base_bootsize_x // (2 ** powers)
    bootsizes_y = base_bootsize_y // (2 ** powers)
    return bootsizes_x, bootsizes_y


def compute_robust_error_metrics(
    estimated: np.ndarray,
    reference: np.ndarray,
    zero_threshold_percentile: float = 5.0
) -> Dict[str, float]:
    """
    Compute robust error metrics that handle negative and near-zero values.

    Uses multiple approaches:
    1. Symmetric MAPE: |a-b| / ((|a|+|b|)/2) - works for negative values
    2. Normalized RMSE: RMSE / range(reference) - scale-independent
    3. R² (coefficient of determination) - correlation-based
    4. Mean Absolute Error (MAE) - for reference

    Parameters
    ----------
    estimated : np.ndarray
        Estimated values
    reference : np.ndarray
        Reference (true) values
    zero_threshold_percentile : float
        Percentile of |reference| below which values are considered "near zero"

    Returns
    -------
    Dict with various error metrics
    """
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", RuntimeWarning)

        # Remove NaN pairs
        mask = ~(np.isnan(estimated) | np.isnan(reference))
        est = estimated[mask]
        ref = reference[mask]

        if len(est) == 0:
            return {
                'symmetric_mape': np.nan,
                'nrmse': np.nan,
                'r_squared': np.nan,
                'mae': np.nan,
                'rmse': np.nan,
                'max_abs_error': np.nan,
                'mean_rel_error': np.nan,  # For backward compatibility
                'max_rel_error': np.nan,
            }

        # Absolute differences
        abs_diff = np.abs(est - ref)

        # 1. Symmetric MAPE (handles negative values)
        # Formula: |a-b| / ((|a|+|b|)/2) * 100
        denominator = (np.abs(est) + np.abs(ref)) / 2
        # Avoid division by zero: use threshold based on data scale
        scale = np.nanpercentile(np.abs(ref), 100 - zero_threshold_percentile)
        threshold = scale * 1e-6 if scale > 0 else 1e-10
        valid_denom = denominator > threshold

        if np.any(valid_denom):
            symmetric_rel_error = np.where(
                valid_denom,
                abs_diff / denominator * 100,
                np.nan
            )
            symmetric_mape = np.nanmean(symmetric_rel_error)
            max_symmetric_rel = np.nanmax(symmetric_rel_error)
        else:
            symmetric_mape = np.nan
            max_symmetric_rel = np.nan

        # 2. RMSE and Normalized RMSE
        rmse = np.sqrt(np.nanmean(abs_diff ** 2))
        ref_range = np.nanmax(ref) - np.nanmin(ref)
        nrmse = (rmse / ref_range * 100) if ref_range > 0 else np.nan

        # 3. R² (coefficient of determination)
        ss_res = np.nansum((est - ref) ** 2)
        ss_tot = np.nansum((ref - np.nanmean(ref)) ** 2)
        r_squared = 1 - (ss_res / ss_tot) if ss_tot > 0 else np.nan

        # 4. MAE
        mae = np.nanmean(abs_diff)

        # 5. Max absolute error
        max_abs_error = np.nanmax(abs_diff)

        # Traditional relative error (for cases where it makes sense)
        # Only compute where reference is not near zero
        ref_scale = np.nanpercentile(np.abs(ref), 90)
        near_zero_threshold = ref_scale * 0.01 if ref_scale > 0 else 1e-10
        valid_ref = np.abs(ref) > near_zero_threshold

        if np.any(valid_ref):
            trad_rel_error = np.where(
                valid_ref,
                abs_diff / np.abs(ref) * 100,
                np.nan
            )
            mean_rel_error = np.nanmean(trad_rel_error)
            max_rel_error = np.nanmax(trad_rel_error)
        else:
            mean_rel_error = symmetric_mape  # Fall back to symmetric
            max_rel_error = max_symmetric_rel

        return {
            'symmetric_mape': symmetric_mape,
            'nrmse': nrmse,
            'r_squared': r_squared,
            'mae': mae,
            'rmse': rmse,
            'max_abs_error': max_abs_error,
            'mean_rel_error': mean_rel_error,  # Backward compatible
            'max_rel_error': max_rel_error,
        }


def get_primary_error_metric(metrics: Dict, config: SFConfig) -> Tuple[str, float]:
    """
    Get the most appropriate error metric based on SF type.

    For advective/scalar (can be negative): use symmetric_mape or nrmse
    For velocity SFs (always positive): use traditional relative error
    """
    if config.fun in ['advective', 'scalar']:
        # Prefer symmetric MAPE, fall back to NRMSE
        if not np.isnan(metrics['symmetric_mape']):
            return 'symmetric_mape', metrics['symmetric_mape']
        else:
            return 'nrmse', metrics['nrmse']
    else:
        # Traditional relative error for positive-definite SFs
        return 'mean_rel_error', metrics['mean_rel_error']


def check_variables_exist(ds, variable_names: List[str]) -> bool:
    """Check if all required variables exist in the dataset."""
    missing = [var for var in variable_names if var not in ds.data_vars]
    if missing:
        print(f"  ⚠ Missing variables: {missing}")
        return False
    return True


def compute_sf_for_config(
    ds,
    config: SFConfig,
    bins: Dict,
    base_bootsize_x: int,
    base_bootsize_y: int,
    bootsizes_x: np.ndarray,
    bootsizes_y: np.ndarray,
    bootstrap_kwargs: Dict[str, Any]
) -> Optional[Dict[str, Any]]:
    """
    Compute structure functions for a single configuration across all bootsizes.

    Returns dict with true_sf, results, comparison_metrics, best_bootsize, best_estimate
    """
    print(f"\n{'='*70}")
    print(f"CONFIG: {config}")
    print(f"{'='*70}")

    # Check if variables exist
    if not check_variables_exist(ds, config.variable_names):
        print(f"  Skipping configuration due to missing variables.")
        return None

    # Base kwargs for this configuration
    base_kwargs = dict(
        ds=ds,
        variables_names=config.variable_names,
        bins=bins,
        fun=config.fun,
        order=config.order,
        **bootstrap_kwargs
    )

    # Compute "true" SF with largest bootsize
    print(f"\n  Computing TRUE SF with bootsize x={base_bootsize_x}, y={base_bootsize_y}...")
    try:
        true_sf = psf.get_isotropic_sf_2d(
            **base_kwargs,
            bootsize={'y': base_bootsize_y, 'x': base_bootsize_x}
        )
    except Exception as e:
        print(f"  ✗ Error computing true SF: {e}")
        return None

    # Store results for each bootsize
    results = {}

    # Test each bootsize
    for bsx, bsy in zip(bootsizes_x, bootsizes_y):
        print(f"  Computing SF with bootsize x={bsx}, y={bsy}...")
        try:
            sf_result = psf.get_isotropic_sf_2d(
                **base_kwargs,
                bootsize={'y': bsy, 'x': bsx}
            )
            results[(bsx, bsy)] = sf_result
        except Exception as e:
            print(f"    ✗ Error: {e}")
            continue

    if not results:
        print(f"  ✗ No valid results for this configuration.")
        return None

    # Extract true SF values
    true_sf_values = true_sf['sf'].values
    r_centers = true_sf['r'].values

    # Compute comparison metrics with robust error handling
    comparison_metrics = {}
    for (bsx, bsy), sf_result in results.items():
        sf_values = sf_result['sf'].values
        metrics = compute_robust_error_metrics(sf_values, true_sf_values)
        comparison_metrics[(bsx, bsy)] = metrics

    # Find best bootsize using appropriate metric for this SF type
    def get_best_metric_value(bs):
        _, value = get_primary_error_metric(comparison_metrics[bs], config)
        return value if not np.isnan(value) else np.inf

    best_bootsize = min(comparison_metrics.keys(), key=get_best_metric_value)
    best_sf = results[best_bootsize]

    # Get the metric name and value used
    metric_name, metric_value = get_primary_error_metric(
        comparison_metrics[best_bootsize], config
    )

    print(f"\n  {'─'*50}")
    print(f"  BEST BOOTSIZE: x={best_bootsize[0]}, y={best_bootsize[1]}")
    print(f"  Primary Metric ({metric_name}): {metric_value:.2f}%")
    print(f"  R²: {comparison_metrics[best_bootsize]['r_squared']:.4f}")
    print(f"  NRMSE: {comparison_metrics[best_bootsize]['nrmse']:.2f}%")
    print(f"  RMSE: {comparison_metrics[best_bootsize]['rmse']:.4e}")
    print(f"  {'─'*50}")

    # Store best estimate
    best_estimate = {
        'bootsize': best_bootsize,
        'sf': best_sf,
        'r': r_centers,
        'sf_values': best_sf['sf'].values,
        'ci_lower': best_sf['ci_lower'].values if 'ci_lower' in best_sf else None,
        'ci_upper': best_sf['ci_upper'].values if 'ci_upper' in best_sf else None,
        'metrics': comparison_metrics[best_bootsize]
    }

    return {
        'config': config,
        'true_sf': true_sf,
        'true_sf_values': true_sf_values,
        'r_centers': r_centers,
        'results': results,
        'comparison_metrics': comparison_metrics,
        'best_bootsize': best_bootsize,
        'best_estimate': best_estimate,
        'base_bootsize': (base_bootsize_x, base_bootsize_y)
    }


def plot_single_config(data: Dict, save_prefix: str = ""):
    """Plot results for a single configuration."""
    config = data['config']
    true_sf = data['true_sf']
    true_sf_values = data['true_sf_values']
    r_centers = data['r_centers']
    results = data['results']
    comparison_metrics = data['comparison_metrics']
    best_bootsize = data['best_bootsize']
    best_estimate = data['best_estimate']
    base_bootsize = data['base_bootsize']

    fig, axes = plt.subplots(1, 2, figsize=(12, 5))
    fig.suptitle(f'{config}', fontsize=14, fontweight='bold')

    # Plot 1: Best estimate vs True SF
    ax1 = axes[0]

    # Check if data has negative values (advective, scalar can be negative)
    has_negative = np.any(true_sf_values < 0) or np.any(best_estimate['sf_values'] < 0)

    if has_negative:
        # Use linear scale for data with negative values
        ax1.plot(r_centers, true_sf_values, 'k-', lw=2.5,
                 label=f'True SF ({base_bootsize[0]}×{base_bootsize[1]})')
        if 'ci_lower' in true_sf and 'ci_upper' in true_sf:
            ax1.fill_between(r_centers,
                             true_sf['ci_lower'].values,
                             true_sf['ci_upper'].values,
                             alpha=0.3, color='black', label='True 95% CI')

        ax1.plot(r_centers, best_estimate['sf_values'], 'r--', lw=2,
                 label=f'Best estimate ({best_bootsize[0]}×{best_bootsize[1]})')
        if best_estimate['ci_lower'] is not None:
            ax1.fill_between(r_centers,
                             best_estimate['ci_lower'],
                             best_estimate['ci_upper'],
                             alpha=0.3, color='red', label='Best 95% CI')

        ax1.set_xscale('log')
        # Add zero line for reference
        ax1.axhline(y=0, color='gray', linestyle=':', alpha=0.5)
    else:
        # Use log-log for positive-definite data
        ax1.loglog(r_centers, true_sf_values, 'k-', lw=2.5,
                   label=f'True SF ({base_bootsize[0]}×{base_bootsize[1]})')
        if 'ci_lower' in true_sf and 'ci_upper' in true_sf:
            ax1.fill_between(r_centers,
                             true_sf['ci_lower'].values,
                             true_sf['ci_upper'].values,
                             alpha=0.3, color='black', label='True 95% CI')

        ax1.loglog(r_centers, best_estimate['sf_values'], 'r--', lw=2,
                   label=f'Best estimate ({best_bootsize[0]}×{best_bootsize[1]})')
        if best_estimate['ci_lower'] is not None:
            ax1.fill_between(r_centers,
                             best_estimate['ci_lower'],
                             best_estimate['ci_upper'],
                             alpha=0.3, color='red', label='Best 95% CI')

    ax1.set_xlabel('r [m]', fontsize=12)
    ax1.set_ylabel(f'SF$_{config.order}$ [{get_units(config)}]', fontsize=12)
    ax1.set_title('Best Estimate vs True SF', fontsize=12)
    ax1.legend(loc='best', fontsize=9)
    ax1.grid(True, alpha=0.3, which='both')

    # Plot 2: Error for all bootsizes (use appropriate metric)
    ax2 = axes[1]
    colors = plt.cm.plasma(np.linspace(0.1, 0.9, len(results)))

    # Determine if we should use symmetric error (for negative-capable SFs)
    use_symmetric = config.fun in ['advective', 'scalar']

    sorted_keys = sorted(results.keys(), key=lambda x: x[0], reverse=True)
    for i, (bsx, bsy) in enumerate(sorted_keys):
        sf_values = results[(bsx, bsy)]['sf'].values

        with warnings.catch_warnings():
            warnings.simplefilter("ignore", RuntimeWarning)

            if use_symmetric:
                # Symmetric relative error: |a-b| / ((|a|+|b|)/2) * 100
                denominator = (np.abs(sf_values) + np.abs(true_sf_values)) / 2
                # Threshold to avoid division by tiny numbers
                scale = np.nanpercentile(np.abs(true_sf_values), 95)
                threshold = scale * 1e-6 if scale > 0 else 1e-10
                error = np.where(
                    denominator > threshold,
                    np.abs(sf_values - true_sf_values) / denominator * 100,
                    np.nan
                )
            else:
                # Traditional relative error
                error = (np.abs(sf_values - true_sf_values)
                        / np.abs(true_sf_values) * 100)

        lw = 2.5 if (bsx, bsy) == best_bootsize else 1.2
        ls = '-' if (bsx, bsy) == best_bootsize else '--'
        label = f'{bsx}×{bsy}' + (' (BEST)' if (bsx, bsy) == best_bootsize else '')
        ax2.semilogx(r_centers, error, ls, color=colors[i], lw=lw, label=label)

    error_type = 'Symmetric Relative' if use_symmetric else 'Relative'
    ax2.set_xlabel('r [m]', fontsize=12)
    ax2.set_ylabel(f'{error_type} Error [%]', fontsize=12)
    ax2.set_title(f'{error_type} Error vs True SF', fontsize=12)
    ax2.legend(loc='best', fontsize=9)
    ax2.grid(True, alpha=0.3, which='both')

    plt.tight_layout()

    # Save figure
    safe_name = config.name.lower().replace(' ', '_').replace('(', '').replace(')', '')
    filename = f'{save_prefix}bootsize_sensitivity_{safe_name}.png'
    plt.savefig(filename, dpi=150, bbox_inches='tight')
    print(f"  Saved: {filename}")

    return fig


def plot_summary(all_results: List[Dict], save_prefix: str = ""):
    """Plot summary comparison across all configurations."""
    n_configs = len(all_results)
    if n_configs == 0:
        print("No results to plot.")
        return None

    # Summary bar plot of best bootsizes and errors
    fig, axes = plt.subplots(1, 3, figsize=(18, 5))

    config_names = [r['config'].name for r in all_results]
    best_errors = []
    for r in all_results:
        _, value = get_primary_error_metric(r['best_estimate']['metrics'], r['config'])
        best_errors.append(value)
    best_bootsizes = [f"{r['best_bootsize'][0]}×{r['best_bootsize'][1]}" for r in all_results]

    # Plot 1: Primary error metric (type-appropriate)
    ax1 = axes[0]
    bars = ax1.barh(config_names, best_errors, color='steelblue', edgecolor='navy')
    ax1.set_xlabel('Primary Error Metric [%]', fontsize=12)
    ax1.set_title('Best Bootsize Performance by Configuration', fontsize=12)
    ax1.grid(True, alpha=0.3, axis='x')

    # Add bootsize labels on bars
    for bar, bs in zip(bars, best_bootsizes):
        width = bar.get_width()
        ax1.annotate(f'{bs}',
                    xy=(width, bar.get_y() + bar.get_height()/2),
                    xytext=(3, 0), textcoords="offset points",
                    ha='left', va='center', fontsize=9)

    # Plot 2: All configurations SF comparison (normalized)
    ax2 = axes[1]
    colors = plt.cm.tab10(np.linspace(0, 1, n_configs))

    for i, result in enumerate(all_results):
        r = result['r_centers']
        sf = result['best_estimate']['sf_values']
        # Normalize by max absolute value for comparison
        sf_norm = sf / np.nanmax(np.abs(sf))
        ax2.semilogx(r, sf_norm, '-', color=colors[i], lw=1.5,
                   label=result['config'].name[:25])

    ax2.axhline(y=0, color='gray', linestyle=':', alpha=0.5)
    ax2.set_xlabel('r [m]', fontsize=12)
    ax2.set_ylabel('Normalized SF', fontsize=12)
    ax2.set_title('Normalized Best Estimates (All Configs)', fontsize=12)
    ax2.legend(loc='best', fontsize=8)
    ax2.grid(True, alpha=0.3, which='both')

    # Plot 3: Bootsize ranking heatmap
    ax3 = axes[2]

    # Collect all bootsizes and create ranking matrix
    all_bootsizes = set()
    for result in all_results:
        all_bootsizes.update(result['comparison_metrics'].keys())
    all_bootsizes = sorted(all_bootsizes, key=lambda x: x[0], reverse=True)

    # Create error matrix (bootsizes x configs)
    error_matrix = np.zeros((len(all_bootsizes), n_configs))
    for j, result in enumerate(all_results):
        config = result['config']
        for i, bs in enumerate(all_bootsizes):
            if bs in result['comparison_metrics']:
                _, err = get_primary_error_metric(result['comparison_metrics'][bs], config)
                error_matrix[i, j] = err
            else:
                error_matrix[i, j] = np.nan

    # Plot heatmap
    im = ax3.imshow(error_matrix, aspect='auto', cmap='RdYlGn_r')
    ax3.set_xticks(range(n_configs))
    ax3.set_xticklabels([c[:15] for c in config_names], rotation=45, ha='right', fontsize=8)
    ax3.set_yticks(range(len(all_bootsizes)))
    ax3.set_yticklabels([f"{bs[0]}×{bs[1]}" for bs in all_bootsizes], fontsize=9)
    ax3.set_xlabel('Configuration', fontsize=12)
    ax3.set_ylabel('Bootsize', fontsize=12)
    ax3.set_title('Error [%] by Bootsize × Config', fontsize=12)

    # Add colorbar
    cbar = plt.colorbar(im, ax=ax3, shrink=0.8)
    cbar.set_label('Error [%]', fontsize=10)

    # Mark best bootsize for each config
    for j, result in enumerate(all_results):
        best_bs = result['best_bootsize']
        if best_bs in all_bootsizes:
            i = all_bootsizes.index(best_bs)
            ax3.plot(j, i, 'k*', markersize=12)

    plt.tight_layout()

    filename = f'{save_prefix}bootsize_sensitivity_summary.png'
    plt.savefig(filename, dpi=150, bbox_inches='tight')
    print(f"\nSaved summary: {filename}")

    return fig


def get_units(config: SFConfig) -> str:
    """Get appropriate units string for the SF type."""
    if config.fun in ['longitudinal', 'transverse', 'default_vel']:
        if config.order == 2:
            return 'm$^2$/s$^2$'
        elif config.order == 3:
            return 'm$^3$/s$^3$'
    elif config.fun == 'scalar':
        return 'units$^{' + str(config.order) + '}$'
    elif config.fun == 'advective':
        return 'm$^{' + str(config.order+1) + '}$/s$^{' + str(config.order+1) + '}$'
    return ''


def print_summary_table(all_results: List[Dict]):
    """Print summary table of all results."""
    print("\n" + "="*95)
    print("SUMMARY TABLE - ALL CONFIGURATIONS")
    print("="*95)
    print(f"{'Configuration':<30} {'Best Bootsize':<14} {'Primary Err %':<14} {'R²':<10} {'NRMSE %':<10}")
    print("-"*95)

    for result in all_results:
        config = result['config']
        config_name = config.name[:28]
        bs = result['best_bootsize']
        metrics = result['best_estimate']['metrics']

        # Get primary metric for this config type
        metric_name, metric_value = get_primary_error_metric(metrics, config)
        r2 = metrics['r_squared']
        nrmse = metrics['nrmse']

        print(f"{config_name:<30} {bs[0]}×{bs[1]:<9} "
              f"{metric_value:<14.2f} {r2:<10.4f} {nrmse:<10.2f}")

    print("="*95)


def print_detailed_table(data: Dict):
    """Print detailed table for a single configuration."""
    config = data['config']
    results = data['results']
    comparison_metrics = data['comparison_metrics']
    best_bootsize = data['best_bootsize']

    # Determine which metric to show based on SF type
    use_symmetric = config.fun in ['advective', 'scalar']
    metric_label = 'Sym.MAPE %' if use_symmetric else 'Rel.Err %'

    print(f"\n  DETAILED TABLE: {config.name}")
    print("  " + "-"*70)
    print(f"  {'Bootsize (x,y)':<16} {metric_label:<14} {'NRMSE %':<12} {'R²':<12}")
    print("  " + "-"*70)

    for bsx, bsy in sorted(results.keys(), key=lambda x: x[0], reverse=True):
        marker = " <-- BEST" if (bsx, bsy) == best_bootsize else ""
        m = comparison_metrics[(bsx, bsy)]

        if use_symmetric:
            primary_err = m['symmetric_mape']
        else:
            primary_err = m['mean_rel_error']

        print(f"  {bsx}×{bsy:<12} "
              f"{primary_err:<14.2f} "
              f"{m['nrmse']:<12.2f} "
              f"{m['r_squared']:<12.4f}{marker}")

    print("  " + "-"*70)


def find_overall_optimal_bootsize(
    all_results: List[Dict],
    method: str = 'mean_rank'
) -> Dict[str, Any]:
    """
    Find the single best bootsize that works well across ALL configurations.

    Parameters
    ----------
    all_results : List[Dict]
        Results from all configurations
    method : str
        Aggregation method:
        - 'mean_rank': Average rank across configs (lower is better)
        - 'mean_error': Average primary error across configs
        - 'max_error': Minimize the worst-case error across configs
        - 'mean_r2': Average R² across configs (higher is better)

    Returns
    -------
    Dict with optimal bootsize and analysis details
    """
    if not all_results:
        return None

    # Collect all bootsizes tested
    all_bootsizes = set()
    for result in all_results:
        all_bootsizes.update(result['comparison_metrics'].keys())
    all_bootsizes = sorted(all_bootsizes, key=lambda x: x[0], reverse=True)

    # Build a matrix of metrics: rows=bootsizes, cols=configs
    bootsize_scores = {bs: {'errors': [], 'r2s': [], 'ranks': []} for bs in all_bootsizes}

    for result in all_results:
        config = result['config']
        metrics_dict = result['comparison_metrics']

        # Get primary error for each bootsize in this config
        errors_for_config = {}
        for bs, metrics in metrics_dict.items():
            _, error_value = get_primary_error_metric(metrics, config)
            errors_for_config[bs] = error_value
            bootsize_scores[bs]['errors'].append(error_value)
            bootsize_scores[bs]['r2s'].append(metrics['r_squared'])

        # Compute ranks for this config (1 = best)
        sorted_bs = sorted(errors_for_config.keys(),
                          key=lambda x: errors_for_config[x] if not np.isnan(errors_for_config[x]) else np.inf)
        for rank, bs in enumerate(sorted_bs, 1):
            bootsize_scores[bs]['ranks'].append(rank)

    # Compute aggregate scores
    aggregate_scores = {}
    for bs, scores in bootsize_scores.items():
        errors = [e for e in scores['errors'] if not np.isnan(e)]
        r2s = [r for r in scores['r2s'] if not np.isnan(r)]
        ranks = scores['ranks']

        aggregate_scores[bs] = {
            'mean_error': np.mean(errors) if errors else np.inf,
            'max_error': np.max(errors) if errors else np.inf,
            'mean_r2': np.mean(r2s) if r2s else -np.inf,
            'mean_rank': np.mean(ranks) if ranks else np.inf,
            'n_configs': len(errors)
        }

    # Select optimal based on method
    if method == 'mean_rank':
        optimal_bs = min(aggregate_scores.keys(),
                        key=lambda x: aggregate_scores[x]['mean_rank'])
    elif method == 'mean_error':
        optimal_bs = min(aggregate_scores.keys(),
                        key=lambda x: aggregate_scores[x]['mean_error'])
    elif method == 'max_error':
        optimal_bs = min(aggregate_scores.keys(),
                        key=lambda x: aggregate_scores[x]['max_error'])
    elif method == 'mean_r2':
        optimal_bs = max(aggregate_scores.keys(),
                        key=lambda x: aggregate_scores[x]['mean_r2'])
    else:
        raise ValueError(f"Unknown method: {method}")

    # Check how many configs agree with this choice
    agreement_count = sum(
        1 for result in all_results
        if result['best_bootsize'] == optimal_bs
    )

    return {
        'optimal_bootsize': optimal_bs,
        'method': method,
        'aggregate_scores': aggregate_scores,
        'optimal_scores': aggregate_scores[optimal_bs],
        'agreement': f"{agreement_count}/{len(all_results)} configs",
        'all_bootsizes_ranked': sorted(
            aggregate_scores.keys(),
            key=lambda x: aggregate_scores[x][method if method != 'mean_r2' else 'mean_rank']
        )
    }


def print_overall_optimal(optimal_result: Dict, all_results: List[Dict]):
    """Print the overall optimal bootsize analysis."""
    if optimal_result is None:
        print("\nNo results to analyze.")
        return

    opt_bs = optimal_result['optimal_bootsize']
    scores = optimal_result['aggregate_scores']

    print("\n" + "="*95)
    print("★ OVERALL OPTIMAL BOOTSIZE ANALYSIS ★")
    print("="*95)

    print(f"\n  Method: {optimal_result['method']}")
    print(f"  ★ OPTIMAL BOOTSIZE: {opt_bs[0]} × {opt_bs[1]}")
    print(f"  Agreement: {optimal_result['agreement']} configurations chose this as their best")

    opt_scores = optimal_result['optimal_scores']
    print(f"\n  Aggregate metrics for optimal bootsize:")
    print(f"    Mean Error:  {opt_scores['mean_error']:.2f}%")
    print(f"    Max Error:   {opt_scores['max_error']:.2f}%")
    print(f"    Mean R²:     {opt_scores['mean_r2']:.4f}")
    print(f"    Mean Rank:   {opt_scores['mean_rank']:.1f}")

    # Ranking table
    print(f"\n  {'Bootsize':<14} {'Mean Err %':<12} {'Max Err %':<12} {'Mean R²':<10} {'Mean Rank':<10}")
    print("  " + "-"*70)

    for bs in optimal_result['all_bootsizes_ranked']:
        s = scores[bs]
        marker = " ★ OPTIMAL" if bs == opt_bs else ""
        print(f"  {bs[0]}×{bs[1]:<9} {s['mean_error']:<12.2f} {s['max_error']:<12.2f} "
              f"{s['mean_r2']:<10.4f} {s['mean_rank']:<10.1f}{marker}")

    print("  " + "-"*70)

    # Show per-config performance with optimal bootsize
    print(f"\n  Performance of optimal bootsize ({opt_bs[0]}×{opt_bs[1]}) per configuration:")
    print("  " + "-"*70)
    print(f"  {'Configuration':<30} {'Error %':<12} {'R²':<10} {'Is Best?':<10}")
    print("  " + "-"*70)

    for result in all_results:
        config = result['config']
        if opt_bs in result['comparison_metrics']:
            metrics = result['comparison_metrics'][opt_bs]
            _, error = get_primary_error_metric(metrics, config)
            r2 = metrics['r_squared']
            is_best = "✓ YES" if result['best_bootsize'] == opt_bs else "no"
            print(f"  {config.name[:28]:<30} {error:<12.2f} {r2:<10.4f} {is_best}")
        else:
            print(f"  {config.name[:28]:<30} {'N/A':<12} {'N/A':<10} {'N/A'}")

    print("="*95)


# =============================================================================
# MAIN ANALYSIS FUNCTION
# =============================================================================

def run_bootsize_sensitivity_analysis(
    ds,
    configs: List[SFConfig] = None,
    base_bootsize_x: int = 240,
    base_bootsize_y: int = 319,
    nbins: int = 11,
    r_min: float = 1.0e3,
    r_max: float = 1.0e6,
    initial_nbootstrap: int = 6000,
    max_nbootstrap: int = 6000,
    step_nbootstrap: int = 0,
    convergence_eps: float = 0.001,
    confidence_interval: float = 0.95,
    n_jobs: int = -1,
    backend: str = 'loky',
    seed: int = 42,
    save_plots: bool = True,
    save_prefix: str = "",
    show_plots: bool = True
) -> Dict[str, Any]:
    """
    Run bootsize sensitivity analysis for multiple SF configurations.

    Parameters
    ----------
    ds : xarray.Dataset
        Input dataset with required variables
    configs : List[SFConfig], optional
        List of configurations to test. If None, uses SF_CONFIGS.
    base_bootsize_x, base_bootsize_y : int
        Base bootsize for "true" SF computation
    nbins : int
        Number of bins for separation distances
    r_min, r_max : float
        Min and max separation distances [m]
    initial_nbootstrap, max_nbootstrap, step_nbootstrap : int
        Bootstrap parameters
    convergence_eps : float
        Convergence tolerance
    confidence_interval : float
        Confidence interval level
    n_jobs : int
        Number of parallel jobs (-1 for all cores)
    backend : str
        Parallel backend
    seed : int
        Random seed for reproducibility
    save_plots : bool
        Whether to save plots
    save_prefix : str
        Prefix for saved files
    show_plots : bool
        Whether to display plots

    Returns
    -------
    Dict with all results, summary, and figures
    """
    if configs is None:
        configs = SF_CONFIGS

    # Define bins
    bin_r = np.logspace(np.log10(r_min), np.log10(r_max), int(nbins + 1))
    bins = {'r': bin_r}

    # Generate bootsizes
    bootsizes_x, bootsizes_y = generate_bootsizes(base_bootsize_x, base_bootsize_y)

    print("="*70)
    print("BOOTSIZE SENSITIVITY ANALYSIS")
    print("="*70)
    print(f"\nBase bootsize: {base_bootsize_x} × {base_bootsize_y}")
    print(f"Testing bootsizes (x, y):")
    for bsx, bsy in zip(bootsizes_x, bootsizes_y):
        print(f"  {bsx} × {bsy}")
    print(f"\nNumber of configurations to test: {len(configs)}")
    for i, cfg in enumerate(configs):
        print(f"  {i+1}. {cfg}")

    # Bootstrap kwargs
    bootstrap_kwargs = dict(
        initial_nbootstrap=initial_nbootstrap,
        max_nbootstrap=max_nbootstrap,
        step_nbootstrap=step_nbootstrap,
        convergence_eps=convergence_eps,
        confidence_interval=confidence_interval,
        n_jobs=n_jobs,
        backend=backend,
        seed=seed
    )

    # Run analysis for each configuration
    all_results = []
    figures = []

    for config in configs:
        result = compute_sf_for_config(
            ds=ds,
            config=config,
            bins=bins,
            base_bootsize_x=base_bootsize_x,
            base_bootsize_y=base_bootsize_y,
            bootsizes_x=bootsizes_x,
            bootsizes_y=bootsizes_y,
            bootstrap_kwargs=bootstrap_kwargs
        )

        if result is not None:
            all_results.append(result)

            # Print detailed table
            print_detailed_table(result)

            # Plot individual results
            if save_plots or show_plots:
                fig = plot_single_config(result, save_prefix=save_prefix)
                figures.append(fig)

    # Print summary
    if all_results:
        print_summary_table(all_results)

        # Find overall optimal bootsize
        optimal_result = find_overall_optimal_bootsize(all_results, method='mean_rank')
        print_overall_optimal(optimal_result, all_results)

        # Plot summary
        if save_plots or show_plots:
            summary_fig = plot_summary(all_results, save_prefix=save_prefix)
            figures.append(summary_fig)
    else:
        optimal_result = None

    if show_plots:
        plt.show()

    return {
        'all_results': all_results,
        'figures': figures,
        'configs': configs,
        'bootsizes': list(zip(bootsizes_x, bootsizes_y)),
        'base_bootsize': (base_bootsize_x, base_bootsize_y),
        'overall_optimal': optimal_result
    }


# =============================================================================
# EXAMPLE USAGE
# =============================================================================

if __name__ == "__main__":
    # Example usage - uncomment and modify as needed

    # # Load your dataset
    # import xarray as xr
    ds = xr.open_dataset('/home/aayouche/Downloads/Gi_example_output.nc')


    # # Run full analysis with all configs
    results = run_bootsize_sensitivity_analysis(
         ds=ds,
         base_bootsize_x=240,
         base_bootsize_y=319,
         save_prefix='analysis_',
         show_plots=False
     )

    # # Access the OVERALL OPTIMAL bootsize (works across all configs)
    # optimal = results['overall_optimal']
    # print(f"Overall optimal bootsize: {optimal['optimal_bootsize']}")
    # print(f"Mean error: {optimal['optimal_scores']['mean_error']:.2f}%")
    # print(f"Mean R²: {optimal['optimal_scores']['mean_r2']:.4f}")

    # # Or run with specific configs only
    # selected_configs = [
    #     SFConfig(name="Longitudinal SF2", fun='longitudinal', order=2, variable_names=['u', 'v']),
    #     SFConfig(name="Scalar SF1", fun='scalar', order=1, variable_names=['E']),
    # ]
    # results = run_bootsize_sensitivity_analysis(
    #     ds=ds,
    #     configs=selected_configs,
    #     save_prefix='selected_'
    # )

    print("Script loaded. Import and call run_bootsize_sensitivity_analysis() with your dataset.")
    print("\nExample:")
    print("  from bootsize_sensitivity_analysis import run_bootsize_sensitivity_analysis, SFConfig")
    print("  results = run_bootsize_sensitivity_analysis(ds=your_dataset)")
    print("\n  # Get overall optimal bootsize:")
    print("  optimal_bs = results['overall_optimal']['optimal_bootsize']")
    print("  print(f'Use bootsize: {optimal_bs[0]} x {optimal_bs[1]}')")
======================================================================
BOOTSIZE SENSITIVITY ANALYSIS
======================================================================

Base bootsize: 240 × 319
Testing bootsizes (x, y):
  60 × 79
  30 × 39
  15 × 19
  7 × 9
  3 × 4

Number of configurations to test: 7
  1. Longitudinal velocity SF2 (longitudinal, order=2)
  2. Transverse velocity SF2 (transverse, order=2)
  3. Default velocity SF2 (default_vel, order=2)
  4. Scalar SF1 (scalar, order=1)
  5. Advective (E gradients) (advective, order=1)
  6. Advective (pressure) (advective, order=1)
  7. Advective (stress) (advective, order=1)

======================================================================
CONFIG: Longitudinal velocity SF2 (longitudinal, order=2)
======================================================================

  Computing TRUE SF with bootsize x=240, y=319...
Dimensions ('y', 'x') are already in the expected order
Dimension y has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Dimension x has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Using bootsize: {'y': 319, 'x': 240}
Bootstrappable dimensions: []
No bootstrappable dimensions available. Structure functions will be calculated using the full dataset without bootstrapping or spacings.

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: longitudinal
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


No bootstrappable dimensions available. Calculating structure function once with full dataset.
  Computing SF with bootsize x=60, y=79...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(79), 'x': np.int64(60)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: longitudinal
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 2000 bootstraps
Processing spacing 2 with 2000 bootstraps
Processing spacing 4 with 2000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 34668360000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=30, y=39...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(39), 'x': np.int64(30)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: longitudinal
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1500 bootstraps
Processing spacing 2 with 1500 bootstraps
Processing spacing 4 with 1500 bootstraps
Processing spacing 8 with 1500 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 2169180000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=15, y=19...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(19), 'x': np.int64(15)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: longitudinal
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1200 bootstraps
Processing spacing 2 with 1200 bootstraps
Processing spacing 4 with 1200 bootstraps
Processing spacing 8 with 1200 bootstraps
Processing spacing 16 with 1200 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 135090000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=7, y=9...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(9), 'x': np.int64(7)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: longitudinal
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1000 bootstraps
Processing spacing 2 with 1000 bootstraps
Processing spacing 4 with 1000 bootstraps
Processing spacing 8 with 1000 bootstraps
Processing spacing 16 with 1000 bootstraps
Processing spacing 32 with 1000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 7182000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=3, y=4...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(4), 'x': np.int64(3)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32, 64]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: longitudinal
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 857 bootstraps
Processing spacing 2 with 857 bootstraps
Processing spacing 4 with 857 bootstraps
Processing spacing 8 with 857 bootstraps
Processing spacing 16 with 857 bootstraps
Processing spacing 32 with 857 bootstraps
Processing spacing 64 with 857 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 287952
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================

  ──────────────────────────────────────────────────
  BEST BOOTSIZE: x=30, y=39
  Primary Metric (mean_rel_error): 3.27%
  R²: 0.9940
  NRMSE: 2.91%
  RMSE: 7.8558e-04
  ──────────────────────────────────────────────────

  DETAILED TABLE: Longitudinal velocity SF2
  ----------------------------------------------------------------------
  Bootsize (x,y)   Rel.Err %      NRMSE %      R²
  ----------------------------------------------------------------------
  60×79           5.11           3.47         0.9915
  30×39           3.27           2.91         0.9940       <-- BEST
  15×19           3.57           3.26         0.9925
  7×9            5.55           6.09         0.9740
  3×4            8.29           6.76         0.9679
  ----------------------------------------------------------------------
  Saved: analysis_bootsize_sensitivity_longitudinal_velocity_sf2.png

======================================================================
CONFIG: Transverse velocity SF2 (transverse, order=2)
======================================================================

  Computing TRUE SF with bootsize x=240, y=319...
Dimensions ('y', 'x') are already in the expected order
Dimension y has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Dimension x has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Using bootsize: {'y': 319, 'x': 240}
Bootstrappable dimensions: []
No bootstrappable dimensions available. Structure functions will be calculated using the full dataset without bootstrapping or spacings.

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: transverse
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


No bootstrappable dimensions available. Calculating structure function once with full dataset.
  Computing SF with bootsize x=60, y=79...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(79), 'x': np.int64(60)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: transverse
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 2000 bootstraps
Processing spacing 2 with 2000 bootstraps
Processing spacing 4 with 2000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 34668360000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=30, y=39...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(39), 'x': np.int64(30)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: transverse
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1500 bootstraps
Processing spacing 2 with 1500 bootstraps
Processing spacing 4 with 1500 bootstraps
Processing spacing 8 with 1500 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 2169180000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=15, y=19...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(19), 'x': np.int64(15)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: transverse
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1200 bootstraps
Processing spacing 2 with 1200 bootstraps
Processing spacing 4 with 1200 bootstraps
Processing spacing 8 with 1200 bootstraps
Processing spacing 16 with 1200 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 135090000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=7, y=9...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(9), 'x': np.int64(7)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: transverse
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1000 bootstraps
Processing spacing 2 with 1000 bootstraps
Processing spacing 4 with 1000 bootstraps
Processing spacing 8 with 1000 bootstraps
Processing spacing 16 with 1000 bootstraps
Processing spacing 32 with 1000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 7182000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=3, y=4...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(4), 'x': np.int64(3)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32, 64]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: transverse
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 857 bootstraps
Processing spacing 2 with 857 bootstraps
Processing spacing 4 with 857 bootstraps
Processing spacing 8 with 857 bootstraps
Processing spacing 16 with 857 bootstraps
Processing spacing 32 with 857 bootstraps
Processing spacing 64 with 857 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 287952
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================

  ──────────────────────────────────────────────────
  BEST BOOTSIZE: x=30, y=39
  Primary Metric (mean_rel_error): 6.62%
  R²: 0.9692
  NRMSE: 6.32%
  RMSE: 1.7087e-03
  ──────────────────────────────────────────────────

  DETAILED TABLE: Transverse velocity SF2
  ----------------------------------------------------------------------
  Bootsize (x,y)   Rel.Err %      NRMSE %      R²
  ----------------------------------------------------------------------
  60×79           8.06           7.29         0.9590
  30×39           6.62           6.32         0.9692       <-- BEST
  15×19           8.00           7.02         0.9620
  7×9            10.49          8.56         0.9435
  3×4            12.38          9.37         0.9323
  ----------------------------------------------------------------------
  Saved: analysis_bootsize_sensitivity_transverse_velocity_sf2.png

======================================================================
CONFIG: Default velocity SF2 (default_vel, order=2)
======================================================================

  Computing TRUE SF with bootsize x=240, y=319...
Dimensions ('y', 'x') are already in the expected order
Dimension y has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Dimension x has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Using bootsize: {'y': 319, 'x': 240}
Bootstrappable dimensions: []
No bootstrappable dimensions available. Structure functions will be calculated using the full dataset without bootstrapping or spacings.

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: default_vel
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


No bootstrappable dimensions available. Calculating structure function once with full dataset.
  Computing SF with bootsize x=60, y=79...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(79), 'x': np.int64(60)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: default_vel
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 2000 bootstraps
Processing spacing 2 with 2000 bootstraps
Processing spacing 4 with 2000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 34668360000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=30, y=39...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(39), 'x': np.int64(30)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: default_vel
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1500 bootstraps
Processing spacing 2 with 1500 bootstraps
Processing spacing 4 with 1500 bootstraps
Processing spacing 8 with 1500 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 2169180000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=15, y=19...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(19), 'x': np.int64(15)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: default_vel
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1200 bootstraps
Processing spacing 2 with 1200 bootstraps
Processing spacing 4 with 1200 bootstraps
Processing spacing 8 with 1200 bootstraps
Processing spacing 16 with 1200 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 135090000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=7, y=9...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(9), 'x': np.int64(7)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: default_vel
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1000 bootstraps
Processing spacing 2 with 1000 bootstraps
Processing spacing 4 with 1000 bootstraps
Processing spacing 8 with 1000 bootstraps
Processing spacing 16 with 1000 bootstraps
Processing spacing 32 with 1000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 7182000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=3, y=4...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(4), 'x': np.int64(3)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32, 64]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: default_vel
Variables: ['u', 'v'], Order: 2
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 857 bootstraps
Processing spacing 2 with 857 bootstraps
Processing spacing 4 with 857 bootstraps
Processing spacing 8 with 857 bootstraps
Processing spacing 16 with 857 bootstraps
Processing spacing 32 with 857 bootstraps
Processing spacing 64 with 857 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 287952
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================

  ──────────────────────────────────────────────────
  BEST BOOTSIZE: x=30, y=39
  Primary Metric (mean_rel_error): 4.81%
  R²: 0.9841
  NRMSE: 4.80%
  RMSE: 2.4768e-03
  ──────────────────────────────────────────────────

  DETAILED TABLE: Default velocity SF2
  ----------------------------------------------------------------------
  Bootsize (x,y)   Rel.Err %      NRMSE %      R²
  ----------------------------------------------------------------------
  60×79           6.52           5.61         0.9783
  30×39           4.81           4.80         0.9841       <-- BEST
  15×19           5.65           5.27         0.9808
  7×9            7.99           7.54         0.9608
  3×4            10.01          7.76         0.9584
  ----------------------------------------------------------------------
  Saved: analysis_bootsize_sensitivity_default_velocity_sf2.png

======================================================================
CONFIG: Scalar SF1 (scalar, order=1)
======================================================================

  Computing TRUE SF with bootsize x=240, y=319...
Dimensions ('y', 'x') are already in the expected order
Dimension y has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Dimension x has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Using bootsize: {'y': 319, 'x': 240}
Bootstrappable dimensions: []
No bootstrappable dimensions available. Structure functions will be calculated using the full dataset without bootstrapping or spacings.

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: scalar
Variables: ['E'], Order: 1
Confidence level: 0.95
============================================================


No bootstrappable dimensions available. Calculating structure function once with full dataset.
  Computing SF with bootsize x=60, y=79...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(79), 'x': np.int64(60)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: scalar
Variables: ['E'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 2000 bootstraps
Processing spacing 2 with 2000 bootstraps
Processing spacing 4 with 2000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 34668360000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=30, y=39...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(39), 'x': np.int64(30)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: scalar
Variables: ['E'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1500 bootstraps
Processing spacing 2 with 1500 bootstraps
Processing spacing 4 with 1500 bootstraps
Processing spacing 8 with 1500 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 2169180000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=15, y=19...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(19), 'x': np.int64(15)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: scalar
Variables: ['E'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1200 bootstraps
Processing spacing 2 with 1200 bootstraps
Processing spacing 4 with 1200 bootstraps
Processing spacing 8 with 1200 bootstraps
Processing spacing 16 with 1200 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 135090000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=7, y=9...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(9), 'x': np.int64(7)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: scalar
Variables: ['E'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1000 bootstraps
Processing spacing 2 with 1000 bootstraps
Processing spacing 4 with 1000 bootstraps
Processing spacing 8 with 1000 bootstraps
Processing spacing 16 with 1000 bootstraps
Processing spacing 32 with 1000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 7182000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=3, y=4...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(4), 'x': np.int64(3)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32, 64]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: scalar
Variables: ['E'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 857 bootstraps
Processing spacing 2 with 857 bootstraps
Processing spacing 4 with 857 bootstraps
Processing spacing 8 with 857 bootstraps
Processing spacing 16 with 857 bootstraps
Processing spacing 32 with 857 bootstraps
Processing spacing 64 with 857 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 287952
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================

  ──────────────────────────────────────────────────
  BEST BOOTSIZE: x=30, y=39
  Primary Metric (symmetric_mape): 37.49%
  R²: 0.8856
  NRMSE: 10.00%
  RMSE: 7.5352e-04
  ──────────────────────────────────────────────────

  DETAILED TABLE: Scalar SF1
  ----------------------------------------------------------------------
  Bootsize (x,y)   Sym.MAPE %     NRMSE %      R²
  ----------------------------------------------------------------------
  60×79           41.69          8.71         0.9133
  30×39           37.49          10.00        0.8856       <-- BEST
  15×19           72.08          8.84         0.9106
  7×9            76.74          19.34        0.5724
  3×4            166.98         35.40        -0.4327
  ----------------------------------------------------------------------
  Saved: analysis_bootsize_sensitivity_scalar_sf1.png

======================================================================
CONFIG: Advective (E gradients) (advective, order=1)
======================================================================

  Computing TRUE SF with bootsize x=240, y=319...
Dimensions ('y', 'x') are already in the expected order
Dimension y has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Dimension x has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Using bootsize: {'y': 319, 'x': 240}
Bootstrappable dimensions: []
No bootstrappable dimensions available. Structure functions will be calculated using the full dataset without bootstrapping or spacings.

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'w', 'E_dx', 'E_dy', 'E_dz'], Order: 1
Confidence level: 0.95
============================================================


No bootstrappable dimensions available. Calculating structure function once with full dataset.
  Computing SF with bootsize x=60, y=79...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(79), 'x': np.int64(60)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'w', 'E_dx', 'E_dy', 'E_dz'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 2000 bootstraps
Processing spacing 2 with 2000 bootstraps
Processing spacing 4 with 2000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 34668360000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=30, y=39...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(39), 'x': np.int64(30)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'w', 'E_dx', 'E_dy', 'E_dz'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1500 bootstraps
Processing spacing 2 with 1500 bootstraps
Processing spacing 4 with 1500 bootstraps
Processing spacing 8 with 1500 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 2169180000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=15, y=19...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(19), 'x': np.int64(15)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'w', 'E_dx', 'E_dy', 'E_dz'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1200 bootstraps
Processing spacing 2 with 1200 bootstraps
Processing spacing 4 with 1200 bootstraps
Processing spacing 8 with 1200 bootstraps
Processing spacing 16 with 1200 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 135090000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=7, y=9...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(9), 'x': np.int64(7)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'w', 'E_dx', 'E_dy', 'E_dz'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1000 bootstraps
Processing spacing 2 with 1000 bootstraps
Processing spacing 4 with 1000 bootstraps
Processing spacing 8 with 1000 bootstraps
Processing spacing 16 with 1000 bootstraps
Processing spacing 32 with 1000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 7182000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=3, y=4...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(4), 'x': np.int64(3)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32, 64]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'w', 'E_dx', 'E_dy', 'E_dz'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 857 bootstraps
Processing spacing 2 with 857 bootstraps
Processing spacing 4 with 857 bootstraps
Processing spacing 8 with 857 bootstraps
Processing spacing 16 with 857 bootstraps
Processing spacing 32 with 857 bootstraps
Processing spacing 64 with 857 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 287952
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================

  ──────────────────────────────────────────────────
  BEST BOOTSIZE: x=30, y=39
  Primary Metric (symmetric_mape): 17.68%
  R²: 0.9840
  NRMSE: 3.76%
  RMSE: 1.1822e-09
  ──────────────────────────────────────────────────

  DETAILED TABLE: Advective (E gradients)
  ----------------------------------------------------------------------
  Bootsize (x,y)   Sym.MAPE %     NRMSE %      R²
  ----------------------------------------------------------------------
  60×79           19.65          5.82         0.9615
  30×39           17.68          3.76         0.9840       <-- BEST
  15×19           27.95          21.54        0.4729
  7×9            17.80          6.59         0.9507
  3×4            42.47          36.86        -0.5428
  ----------------------------------------------------------------------
  Saved: analysis_bootsize_sensitivity_advective_e_gradients.png

======================================================================
CONFIG: Advective (pressure) (advective, order=1)
======================================================================

  Computing TRUE SF with bootsize x=240, y=319...
Dimensions ('y', 'x') are already in the expected order
Dimension y has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Dimension x has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Using bootsize: {'y': 319, 'x': 240}
Bootstrappable dimensions: []
No bootstrappable dimensions available. Structure functions will be calculated using the full dataset without bootstrapping or spacings.

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'dpdx', 'dpdy'], Order: 1
Confidence level: 0.95
============================================================


No bootstrappable dimensions available. Calculating structure function once with full dataset.
  Computing SF with bootsize x=60, y=79...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(79), 'x': np.int64(60)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'dpdx', 'dpdy'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 2000 bootstraps
Processing spacing 2 with 2000 bootstraps
Processing spacing 4 with 2000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 34668360000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=30, y=39...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(39), 'x': np.int64(30)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'dpdx', 'dpdy'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1500 bootstraps
Processing spacing 2 with 1500 bootstraps
Processing spacing 4 with 1500 bootstraps
Processing spacing 8 with 1500 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 2169180000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=15, y=19...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(19), 'x': np.int64(15)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'dpdx', 'dpdy'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1200 bootstraps
Processing spacing 2 with 1200 bootstraps
Processing spacing 4 with 1200 bootstraps
Processing spacing 8 with 1200 bootstraps
Processing spacing 16 with 1200 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 135090000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=7, y=9...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(9), 'x': np.int64(7)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'dpdx', 'dpdy'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1000 bootstraps
Processing spacing 2 with 1000 bootstraps
Processing spacing 4 with 1000 bootstraps
Processing spacing 8 with 1000 bootstraps
Processing spacing 16 with 1000 bootstraps
Processing spacing 32 with 1000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 7182000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=3, y=4...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(4), 'x': np.int64(3)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32, 64]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'dpdx', 'dpdy'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 857 bootstraps
Processing spacing 2 with 857 bootstraps
Processing spacing 4 with 857 bootstraps
Processing spacing 8 with 857 bootstraps
Processing spacing 16 with 857 bootstraps
Processing spacing 32 with 857 bootstraps
Processing spacing 64 with 857 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 287952
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================

  ──────────────────────────────────────────────────
  BEST BOOTSIZE: x=30, y=39
  Primary Metric (symmetric_mape): 43.99%
  R²: 0.6180
  NRMSE: 20.13%
  RMSE: 1.2656e-08
  ──────────────────────────────────────────────────

  DETAILED TABLE: Advective (pressure)
  ----------------------------------------------------------------------
  Bootsize (x,y)   Sym.MAPE %     NRMSE %      R²
  ----------------------------------------------------------------------
  60×79           46.57          24.57        0.4311
  30×39           43.99          20.13        0.6180       <-- BEST
  15×19           77.99          35.70        -0.2009
  7×9            80.08          58.09        -2.1795
  3×4            83.37          83.77        -5.6123
  ----------------------------------------------------------------------
  Saved: analysis_bootsize_sensitivity_advective_pressure.png

======================================================================
CONFIG: Advective (stress) (advective, order=1)
======================================================================

  Computing TRUE SF with bootsize x=240, y=319...
Dimensions ('y', 'x') are already in the expected order
Dimension y has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Dimension x has bootsize equal to or larger than dimension size. No bootstrapping will be done across this dimension.
Using bootsize: {'y': 319, 'x': 240}
Bootstrappable dimensions: []
No bootstrappable dimensions available. Structure functions will be calculated using the full dataset without bootstrapping or spacings.

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'tau_x', 'tau_y'], Order: 1
Confidence level: 0.95
============================================================


No bootstrappable dimensions available. Calculating structure function once with full dataset.
  Computing SF with bootsize x=60, y=79...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(79), 'x': np.int64(60)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'tau_x', 'tau_y'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 2000 bootstraps
Processing spacing 2 with 2000 bootstraps
Processing spacing 4 with 2000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 34668360000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=30, y=39...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(39), 'x': np.int64(30)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'tau_x', 'tau_y'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1500 bootstraps
Processing spacing 2 with 1500 bootstraps
Processing spacing 4 with 1500 bootstraps
Processing spacing 8 with 1500 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 2169180000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=15, y=19...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(19), 'x': np.int64(15)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'tau_x', 'tau_y'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1200 bootstraps
Processing spacing 2 with 1200 bootstraps
Processing spacing 4 with 1200 bootstraps
Processing spacing 8 with 1200 bootstraps
Processing spacing 16 with 1200 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 135090000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=7, y=9...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(9), 'x': np.int64(7)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'tau_x', 'tau_y'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 1000 bootstraps
Processing spacing 2 with 1000 bootstraps
Processing spacing 4 with 1000 bootstraps
Processing spacing 8 with 1000 bootstraps
Processing spacing 16 with 1000 bootstraps
Processing spacing 32 with 1000 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 7182000
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================
  Computing SF with bootsize x=3, y=4...
Dimensions ('y', 'x') are already in the expected order
Using bootsize: {'y': np.int64(4), 'x': np.int64(3)}
Bootstrappable dimensions: ['y', 'x']
Two bootstrappable dimensions. Available spacings: [1, 2, 4, 8, 16, 32, 64]

============================================================
STARTING ISOTROPIC_SF WITH FUNCTION TYPE: advective
Variables: ['u', 'v', 'tau_x', 'tau_y'], Order: 1
Confidence level: 0.95
============================================================


INITIAL BOOTSTRAP PHASE
Processing spacing 1 with 857 bootstraps
Processing spacing 2 with 857 bootstraps
Processing spacing 4 with 857 bootstraps
Processing spacing 8 with 857 bootstraps
Processing spacing 16 with 857 bootstraps
Processing spacing 32 with 857 bootstraps
Processing spacing 64 with 857 bootstraps

CALCULATING BIN DENSITIES
Total points collected: 287952
Bins with points: 10/11
Marked 1 bins as converged (low_density)
Marked 10 bins as converged (converged_eps)

STARTING ADAPTIVE CONVERGENCE LOOP
All bins have converged or reached max bootstraps!

FINAL CONVERGENCE STATISTICS:
  Total bins with data (>10 points): 10
  Converged bins: 10
  Unconverged bins: 0
  Bins at max bootstraps: 10

Creating output dataset...
ISOTROPIC SF COMPLETED SUCCESSFULLY!
============================================================

  ──────────────────────────────────────────────────
  BEST BOOTSIZE: x=30, y=39
  Primary Metric (symmetric_mape): 58.81%
  R²: 0.8957
  NRMSE: 9.51%
  RMSE: 1.1332e-03
  ──────────────────────────────────────────────────

  DETAILED TABLE: Advective (stress)
  ----------------------------------------------------------------------
  Bootsize (x,y)   Sym.MAPE %     NRMSE %      R²
  ----------------------------------------------------------------------
  60×79           67.28          10.76        0.8664
  30×39           58.81          9.51         0.8957       <-- BEST
  15×19           65.86          9.47         0.8964
  7×9            66.92          10.92        0.8624
  3×4            102.08         19.15        0.5770
  ----------------------------------------------------------------------
  Saved: analysis_bootsize_sensitivity_advective_stress.png

===============================================================================================
SUMMARY TABLE - ALL CONFIGURATIONS
===============================================================================================
Configuration                  Best Bootsize  Primary Err %  R²         NRMSE %
-----------------------------------------------------------------------------------------------
Longitudinal velocity SF2      30×39        3.27           0.9940     2.91
Transverse velocity SF2        30×39        6.62           0.9692     6.32
Default velocity SF2           30×39        4.81           0.9841     4.80
Scalar SF1                     30×39        37.49          0.8856     10.00
Advective (E gradients)        30×39        17.68          0.9840     3.76
Advective (pressure)           30×39        43.99          0.6180     20.13
Advective (stress)             30×39        58.81          0.8957     9.51
===============================================================================================

===============================================================================================
★ OVERALL OPTIMAL BOOTSIZE ANALYSIS ★
===============================================================================================

  Method: mean_rank
  ★ OPTIMAL BOOTSIZE: 30 × 39
  Agreement: 7/7 configs configurations chose this as their best

  Aggregate metrics for optimal bootsize:
    Mean Error:  24.67%
    Max Error:   58.81%
    Mean R²:     0.9044
    Mean Rank:   1.0

  Bootsize       Mean Err %   Max Err %    Mean R²    Mean Rank
  ----------------------------------------------------------------------
  30×39        24.67        58.81        0.9044     1.0        ★ OPTIMAL
  15×19        37.30        77.99        0.7164     2.6
  60×79        27.84        67.28        0.8716     2.9
  7×9         37.94        80.08        0.4406     3.6
  3×4         60.80        166.98       -0.4503    5.0
  ----------------------------------------------------------------------

  Performance of optimal bootsize (30×39) per configuration:
  ----------------------------------------------------------------------
  Configuration                  Error %      R²         Is Best?
  ----------------------------------------------------------------------
  Longitudinal velocity SF2      3.27         0.9940     ✓ YES
  Transverse velocity SF2        6.62         0.9692     ✓ YES
  Default velocity SF2           4.81         0.9841     ✓ YES
  Scalar SF1                     37.49        0.8856     ✓ YES
  Advective (E gradients)        17.68        0.9840     ✓ YES
  Advective (pressure)           43.99        0.6180     ✓ YES
  Advective (stress)             58.81        0.8957     ✓ YES
===============================================================================================

Saved summary: analysis_bootsize_sensitivity_summary.png
Script loaded. Import and call run_bootsize_sensitivity_analysis() with your dataset.

Example:
  from bootsize_sensitivity_analysis import run_bootsize_sensitivity_analysis, SFConfig
  results = run_bootsize_sensitivity_analysis(ds=your_dataset)

  # Get overall optimal bootsize:
  optimal_bs = results['overall_optimal']['optimal_bootsize']
  print(f'Use bootsize: {optimal_bs[0]} x {optimal_bs[1]}')
../_images/diagnostics_find_optimal_bootsize_small_dataset_4_1.png
../_images/diagnostics_find_optimal_bootsize_small_dataset_4_2.png
../_images/diagnostics_find_optimal_bootsize_small_dataset_4_3.png
../_images/diagnostics_find_optimal_bootsize_small_dataset_4_4.png
../_images/diagnostics_find_optimal_bootsize_small_dataset_4_5.png
../_images/diagnostics_find_optimal_bootsize_small_dataset_4_6.png
../_images/diagnostics_find_optimal_bootsize_small_dataset_4_7.png
../_images/diagnostics_find_optimal_bootsize_small_dataset_4_8.png