Menu

src-local/sweep_utils.sh

sweep_utils.sh

#!/bin/bash
# sweep_utils.sh - Shared utility functions for parameter sweep generation
#
# Description:
#   Provides reusable functions for extracting sweep parameters, validating
#   case ranges, and generating Cartesian product combinations of parameters.
#   This library is sourced by sweep scripts to eliminate code duplication.
#
# Functions:
#   extract_sweep_variables <file>     - Extract SWEEP_* variables from file
#   validate_case_range <start> <end>  - Validate 4-digit case number range
#   generate_sweep_combinations        - Generate all parameter combinations
#   setup_sweep_temp_dir [base_dir]    - Create temp directory with cleanup
#
# Usage:
#   source src-local/sweep_utils.sh
#   source "\$SWEEP_FILE"  # Get BASE_CONFIG, CASE_START, CASE_END
#   extract_sweep_variables "\$SWEEP_FILE"
#   validate_case_range "\$CASE_START" "\$CASE_END"
#   setup_sweep_temp_dir "\$SCRIPT_DIR"
#   generate_sweep_combinations
#
# Dependencies:
#   - bash 4.0+ (for arrays and \${!var} syntax)
#   - Standard POSIX utilities (sed, xargs, mktemp)
#
# Author: Vatsal Sanjay
# Organization: CoMPhy Lab, Durham University

# Global arrays populated by extract_sweep_variables
SWEEP_VARS=()
SWEEP_VALUES=()

# Global variables populated by setup_sweep_temp_dir and generate_sweep_combinations
SWEEP_TEMP_DIR=""
SWEEP_CASE_NUM=0
SWEEP_COMBINATION_COUNT=0
SWEEP_CASE_FILES=()

# ============================================================
# Extract SWEEP_* variables from a sweep file
# Sets: SWEEP_VARS[], SWEEP_VALUES[]
# Usage: extract_sweep_variables <sweep_file>
# ============================================================
extract_sweep_variables() {
    local sweep_file="\$1"

    if [ ! -f "\$sweep_file" ]; then
        echo "ERROR: Sweep file not found: \$sweep_file" >&2
        return 1
    fi

    # Reset arrays
    SWEEP_VARS=()
    SWEEP_VALUES=()

    # Read sweep file and extract SWEEP_* variables
    while IFS='=' read -r key value; do
        # Skip comments and empty lines
        [[ "\${key:-}" =~ ^[[:space:]]*# ]] && continue
        [[ -z "\${key:-}" ]] && continue

        # Match SWEEP_* variables
        if [[ "\$key" =~ ^[[:space:]]*SWEEP_([^=]+) ]]; then
            local var_name="\${BASH_REMATCH[1]}"
            # Remove inline comments and whitespace
            value=\$(echo "\${value:-}" | sed 's/#.*//' | xargs)

            SWEEP_VARS+=("\$var_name")
            SWEEP_VALUES+=("\$value")
        fi
    done < "\$sweep_file"

    if [ \${#SWEEP_VARS[@]} -eq 0 ]; then
        echo "ERROR: No SWEEP_* variables found in \$sweep_file" >&2
        return 1
    fi

    return 0
}

# ============================================================
# Validate case number range (4-digit, start <= end)
# Usage: validate_case_range <start> <end>
# ============================================================
validate_case_range() {
    local case_start="\$1"
    local case_end="\$2"

    if [ -z "\$case_start" ] || [ -z "\$case_end" ]; then
        echo "ERROR: CASE_START and CASE_END must be defined" >&2
        return 1
    fi

    if [ "\$case_start" -lt 1000 ] || [ "\$case_start" -gt 9999 ]; then
        echo "ERROR: CASE_START must be 4-digit (1000-9999), got: \$case_start" >&2
        return 1
    fi

    if [ "\$case_end" -lt "\$case_start" ] || [ "\$case_end" -gt 9999 ]; then
        echo "ERROR: CASE_END must be >= CASE_START and <= 9999, got: \$case_end" >&2
        return 1
    fi

    return 0
}

# ============================================================
# Print sweep variables (for debugging/logging)
# Usage: print_sweep_variables
# ============================================================
print_sweep_variables() {
    echo "Sweep variables:"
    for i in "\${!SWEEP_VARS[@]}"; do
        echo "  \${SWEEP_VARS[\$i]} = \${SWEEP_VALUES[\$i]}"
    done
}

# ============================================================
# Setup temporary directory for sweep generation
# Sets: SWEEP_TEMP_DIR
# Usage: setup_sweep_temp_dir [base_dir]
# WARNING: Sets an EXIT trap for cleanup which overrides any existing
#          EXIT trap. Calling scripts should not set their own EXIT traps.
# ============================================================
setup_sweep_temp_dir() {
    local base_dir="\${1:-}"

    if [ -n "\$base_dir" ]; then
        # Use work directory (for HPC - avoids slow /tmp)
        SWEEP_TEMP_DIR="\${base_dir}/.sweep_tmp_\$\$"
        mkdir -p "\$SWEEP_TEMP_DIR" || {
            echo "ERROR: Failed to create temp directory: \$SWEEP_TEMP_DIR" >&2
            return 1
        }
    else
        # Use system temp directory
        SWEEP_TEMP_DIR=\$(mktemp -d "\${TMPDIR:-/tmp}/sweep.XXXXXX")
        if [ \$? -ne 0 ] || [ -z "\$SWEEP_TEMP_DIR" ]; then
            echo "ERROR: Failed to create temp directory with mktemp" >&2
            return 1
        fi
    fi

    # Setup cleanup trap (only after successful temp dir creation)
    trap 'rm -rf "\$SWEEP_TEMP_DIR"' EXIT

    return 0
}

# ============================================================
# Generate all parameter combinations (Cartesian product)
# Requires: SWEEP_VARS[], SWEEP_VALUES[], BASE_CONFIG, CASE_START
# Sets: SWEEP_CASE_FILES[], SWEEP_COMBINATION_COUNT
# Usage: generate_sweep_combinations [verbose]
# ============================================================
generate_sweep_combinations() {
    local verbose="\${1:-0}"

    if [ -z "\${SWEEP_TEMP_DIR:-}" ]; then
        echo "ERROR: Call setup_sweep_temp_dir first" >&2
        return 1
    fi

    if [ -z "\${BASE_CONFIG:-}" ]; then
        echo "ERROR: BASE_CONFIG not defined" >&2
        return 1
    fi

    if [ ! -f "\$BASE_CONFIG" ]; then
        echo "ERROR: Base configuration file not found: \$BASE_CONFIG" >&2
        return 1
    fi

    # Initialize counters
    SWEEP_CASE_NUM=\${CASE_START:-1000}
    SWEEP_COMBINATION_COUNT=0
    SWEEP_CASE_FILES=()

    # Recursive function to generate all combinations
    _generate_combinations_recursive() {
        local depth=\$1
        shift
        local current_values=("\$@")

        if [ \$depth -eq \${#SWEEP_VARS[@]} ]; then
            # Base case: all variables assigned, create parameter file
            local case_file="\${SWEEP_TEMP_DIR}/case_\$(printf "%04d" "\$SWEEP_CASE_NUM").params"

            # Copy base config
            cp "\$BASE_CONFIG" "\$case_file"

            # Override CaseNo
            if grep -q "^CaseNo=" "\$case_file"; then
                sed -i'.bak' "s|^CaseNo=.*|CaseNo=\${SWEEP_CASE_NUM}|" "\$case_file"
            else
                echo "CaseNo=\${SWEEP_CASE_NUM}" >> "\$case_file"
            fi
            rm -f "\${case_file}.bak"

            # Override with sweep values
            for i in "\${!SWEEP_VARS[@]}"; do
                local var="\${SWEEP_VARS[\$i]}"
                local val="\${current_values[\$i]}"

                if grep -q "^\${var}=" "\$case_file"; then
                    sed -i'.bak' "s|^\${var}=.*|\${var}=\${val}|" "\$case_file"
                else
                    echo "\${var}=\${val}" >> "\$case_file"
                fi
                rm -f "\${case_file}.bak"
            done

            SWEEP_CASE_FILES+=("\$case_file")
            SWEEP_COMBINATION_COUNT=\$((SWEEP_COMBINATION_COUNT + 1))

            # Print summary if verbose
            if [ "\$verbose" -eq 1 ]; then
                echo "Case \$SWEEP_CASE_NUM:"
                for i in "\${!SWEEP_VARS[@]}"; do
                    echo "  \${SWEEP_VARS[\$i]} = \${current_values[\$i]}"
                done
                echo ""
            fi

            SWEEP_CASE_NUM=\$((SWEEP_CASE_NUM + 1))
            return
        fi

        # Recursive case: iterate through values for current variable
        local values="\${SWEEP_VALUES[\$depth]}"
        IFS=',' read -r -a value_array <<< "\$values"

        for val in "\${value_array[@]}"; do
            val=\$(echo "\$val" | xargs)  # Trim whitespace
            _generate_combinations_recursive \$((depth + 1)) "\${current_values[@]}" "\$val"
        done
    }

    # Start recursion
    _generate_combinations_recursive 0

    return 0
}

# ============================================================
# Validate combination count matches expected range
# Usage: validate_combination_count <case_start> <case_end>
# ============================================================
validate_combination_count() {
    local case_start="\$1"
    local case_end="\$2"
    local expected_count=\$((case_end - case_start + 1))

    if [ "\$SWEEP_COMBINATION_COUNT" -ne "\$expected_count" ]; then
        echo "WARNING: Generated \$SWEEP_COMBINATION_COUNT combinations, but CASE_END suggests \$expected_count" >&2
        echo "         Consider adjusting CASE_END in sweep file" >&2
    fi

    if [ "\$SWEEP_COMBINATION_COUNT" -gt "\$expected_count" ]; then
        echo "ERROR: Too many combinations (\$SWEEP_COMBINATION_COUNT) for range \$case_start-\$case_end" >&2
        return 1
    fi

    return 0
}

# ============================================================
# Progress Tracking for Sweeps
# ============================================================

# Global variables for progress tracking
SWEEP_PROGRESS_START_TIME=0
SWEEP_PROGRESS_COMPLETED=0
SWEEP_PROGRESS_TOTAL=0

# Initialize progress tracking
# Usage: sweep_progress_init <total_cases>
sweep_progress_init() {
    SWEEP_PROGRESS_TOTAL="\$1"
    SWEEP_PROGRESS_COMPLETED=0
    SWEEP_PROGRESS_START_TIME=\$(date +%s)
}

# Update and display progress
# Usage: sweep_progress_update
# Returns: Prints progress line with ETA
sweep_progress_update() {
    SWEEP_PROGRESS_COMPLETED=\$((SWEEP_PROGRESS_COMPLETED + 1))

    local elapsed=\$((\$(date +%s) - SWEEP_PROGRESS_START_TIME))

    if [ \$SWEEP_PROGRESS_COMPLETED -gt 1 ] && [ \$elapsed -gt 0 ]; then
        local avg_time=\$((elapsed / (SWEEP_PROGRESS_COMPLETED - 1)))
        local remaining=\$((SWEEP_PROGRESS_TOTAL - SWEEP_PROGRESS_COMPLETED))
        local eta=\$((avg_time * remaining))

        # Format ETA as HH:MM:SS
        local eta_h=\$((eta / 3600))
        local eta_m=\$(((eta % 3600) / 60))
        local eta_s=\$((eta % 60))

        printf "Progress: %d/%d cases completed (ETA: %02d:%02d:%02d)\n" \
            "\$SWEEP_PROGRESS_COMPLETED" "\$SWEEP_PROGRESS_TOTAL" \
            "\$eta_h" "\$eta_m" "\$eta_s"
    else
        printf "Progress: %d/%d cases completed\n" \
            "\$SWEEP_PROGRESS_COMPLETED" "\$SWEEP_PROGRESS_TOTAL"
    fi
}