#!/bin/bash
# runParameterSweep.sh
#
# Run an ElasticPinchOff parameter sweep from the repository root.
# The script reads SWEEP_* variables from a sweep config file, generates
# case-specific parameter files with incrementing CaseNo, then runs each case
# sequentially using runSimulation.sh.
set -euo pipefail
SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
usage() {
cat <<'EOF'
Usage: bash runParameterSweep.sh [sweep_file] [--mode in|out] [--exec exec_code] [OPTIONS]
Arguments:
sweep_file Sweep config path (default: sweep.params)
Options:
--mode X Case mode: in or out (default: in)
--exec FILE C source in simulationCases/ (overrides --mode mapping)
--mpi Compile/run each case with MPI via runSimulation.sh
--CPUs N MPI process count for --mpi (default: 4)
-n, --dry-run Show generated parameter combinations only
-v, --verbose Print expanded per-case parameter details
-h, --help Show this help message
EOF
}
trim() {
local s="\$1"
s="\${s#"\${s%%[![:space:]]*}"}"
s="\${s%"\${s##*[![:space:]]}"}"
printf '%s' "\$s"
}
get_param_value() {
local key="\$1"
local file="\$2"
awk -F '=' -v key="\$key" '
/^[[:space:]]*#/ { next }
{
k = \$1
gsub(/^[[:space:]]+|[[:space:]]+\$/, "", k)
if (k == key) {
v = \$2
sub(/[[:space:]]*#.*/, "", v)
gsub(/^[[:space:]]+|[[:space:]]+\$/, "", v)
print v
exit
}
}
' "\$file"
}
set_param_in_file() {
local key="\$1"
local value="\$2"
local file="\$3"
if grep -q "^\${key}=" "\$file"; then
sed -i'.bak' "s|^\${key}=.*|\${key}=\${value}|" "\$file"
else
printf '%s=%s\n' "\$key" "\$value" >> "\$file"
fi
rm -f "\${file}.bak"
}
parse_sweep_variables() {
local file="\$1"
local raw_key raw_value key value
SWEEP_VARS=()
SWEEP_VALUES_RAW=()
while IFS='=' read -r raw_key raw_value || [[ -n "\${raw_key:-}" ]]; do
key="\$(trim "\${raw_key:-}")"
[[ -z "\$key" ]] && continue
[[ "\$key" == \#* ]] && continue
if [[ "\$key" =~ ^SWEEP_([A-Za-z0-9_]+)\$ ]]; then
value="\${raw_value:-}"
value="\${value%%#*}"
value="\$(trim "\$value")"
if [[ -z "\$value" ]]; then
echo "ERROR: Empty value list for \${key} in \$file" >&2
exit 1
fi
SWEEP_VARS+=("\${BASH_REMATCH[1]}")
SWEEP_VALUES_RAW+=("\$value")
fi
done < "\$file"
if [[ \${#SWEEP_VARS[@]} -eq 0 ]]; then
echo "ERROR: No SWEEP_* variables found in \$file" >&2
exit 1
fi
}
generate_combinations() {
local depth="\$1"
shift || true
local current_values=("\$@")
if [[ "\$depth" -eq "\${#SWEEP_VARS[@]}" ]]; then
local case_no="\$CURRENT_CASE_NO"
local case_file="\${TEMP_DIR}/case_\$(printf '%04d' "\$case_no").params"
local i
cp "\$BASE_CONFIG" "\$case_file"
set_param_in_file "CaseNo" "\$case_no" "\$case_file"
for i in "\${!SWEEP_VARS[@]}"; do
set_param_in_file "\${SWEEP_VARS[\$i]}" "\${current_values[\$i]}" "\$case_file"
done
PARAM_FILES+=("\$case_file")
((COMBINATION_COUNT += 1))
((CURRENT_CASE_NO += 1))
if [[ \$DRY_RUN -eq 1 || \$VERBOSE -eq 1 ]]; then
echo "Case \${case_no}:"
for i in "\${!SWEEP_VARS[@]}"; do
echo " \${SWEEP_VARS[\$i]}=\${current_values[\$i]}"
done
echo ""
fi
return
fi
local values="\${SWEEP_VALUES_RAW[\$depth]}"
local value_array=()
local value trimmed_value
IFS=',' read -r -a value_array <<< "\$values"
for value in "\${value_array[@]}"; do
trimmed_value="\$(trim "\$value")"
[[ -z "\$trimmed_value" ]] && continue
if [[ \${#current_values[@]} -gt 0 ]]; then
generate_combinations \$((depth + 1)) "\${current_values[@]}" "\$trimmed_value"
else
generate_combinations \$((depth + 1)) "\$trimmed_value"
fi
done
}
# Defaults
EXEC_CODE=""
EXEC_CODE_SET=0
MODE="in"
MODE_SET=0
SWEEP_FILE="sweep.params"
SWEEP_FILE_SET=0
DRY_RUN=0
VERBOSE=0
USE_MPI=0
MPI_CPUS=4
while [[ \$# -gt 0 ]]; do
case "\$1" in
-h|--help)
usage
exit 0
;;
--exec)
if [[ -z "\${2:-}" ]]; then
echo "ERROR: --exec requires a file name." >&2
usage
exit 1
fi
EXEC_CODE="\$2"
EXEC_CODE_SET=1
shift 2
;;
--exec=*)
EXEC_CODE="\${1#*=}"
EXEC_CODE_SET=1
shift
;;
--mode)
if [[ -z "\${2:-}" ]]; then
echo "ERROR: --mode requires 'in' or 'out'." >&2
usage
exit 1
fi
MODE="\$2"
MODE_SET=1
shift 2
;;
--mode=*)
MODE="\${1#*=}"
MODE_SET=1
shift
;;
--mpi)
USE_MPI=1
shift
;;
--CPUs|--cpus)
if [[ -z "\${2:-}" ]]; then
echo "ERROR: \$1 requires a positive integer value." >&2
usage
exit 1
fi
MPI_CPUS="\$2"
shift 2
;;
--CPUs=*|--cpus=*)
MPI_CPUS="\${1#*=}"
shift
;;
-n|--dry-run)
DRY_RUN=1
shift
;;
-v|--verbose)
VERBOSE=1
shift
;;
--)
shift
break
;;
-*)
echo "ERROR: Unknown option: \$1" >&2
usage
exit 1
;;
*)
if [[ \$SWEEP_FILE_SET -eq 0 ]]; then
SWEEP_FILE="\$1"
SWEEP_FILE_SET=1
shift
else
echo "ERROR: Unexpected argument: \$1" >&2
usage
exit 1
fi
;;
esac
done
if [[ \$# -gt 0 ]]; then
echo "ERROR: Unexpected trailing arguments: \$*" >&2
usage
exit 1
fi
if [[ ! "\$MPI_CPUS" =~ ^[1-9][0-9]*\$ ]]; then
echo "ERROR: --CPUs must be a positive integer, got: \$MPI_CPUS" >&2
exit 1
fi
MODE="\$(printf '%s' "\$MODE" | tr '[:upper:]' '[:lower:]')"
if [[ "\$MODE" != "in" && "\$MODE" != "out" ]]; then
echo "ERROR: --mode must be either 'in' or 'out', got: \$MODE" >&2
exit 1
fi
if [[ \$EXEC_CODE_SET -eq 0 ]]; then
if [[ "\$MODE" == "in" ]]; then
EXEC_CODE="LiquidInThinning.c"
else
EXEC_CODE="LiquidOutThinning.c"
fi
fi
if [[ "\$EXEC_CODE" != *.c ]]; then
EXEC_CODE="\${EXEC_CODE}.c"
fi
if [[ \$MODE_SET -eq 0 ]]; then
case "\$EXEC_CODE" in
LiquidInThinning.c)
MODE="in"
;;
LiquidOutThinning.c)
MODE="out"
;;
esac
fi
if [[ ! "\$SWEEP_FILE" = /* ]]; then
SWEEP_FILE="\${SCRIPT_DIR}/\${SWEEP_FILE}"
fi
if [[ ! -f "\$SWEEP_FILE" ]]; then
echo "ERROR: Sweep file not found: \$SWEEP_FILE" >&2
exit 1
fi
if [[ -f "\${SCRIPT_DIR}/.project_config" ]]; then
# shellcheck disable=SC1091
source "\${SCRIPT_DIR}/.project_config"
fi
if ! command -v qcc >/dev/null 2>&1; then
echo "ERROR: qcc not found in PATH." >&2
echo "Hint: source your Basilisk environment or provide .project_config." >&2
exit 1
fi
RUN_SIM_SCRIPT="\${SCRIPT_DIR}/runSimulation.sh"
if [[ ! -f "\$RUN_SIM_SCRIPT" ]]; then
echo "ERROR: runSimulation.sh not found at \${RUN_SIM_SCRIPT}" >&2
exit 1
fi
SRC_FILE_ORIG="\${SCRIPT_DIR}/simulationCases/\${EXEC_CODE}"
if [[ ! -f "\$SRC_FILE_ORIG" ]]; then
echo "ERROR: Source file not found: \$SRC_FILE_ORIG" >&2
exit 1
fi
CONFIG_DIR="\$(cd "\$(dirname "\$SWEEP_FILE")" && pwd)"
# shellcheck disable=SC1090
source "\$SWEEP_FILE"
BASE_CONFIG="\${BASE_CONFIG:-default.params}"
CASE_START="\${CASE_START:-1000}"
if [[ "\$BASE_CONFIG" != /* ]]; then
BASE_CONFIG="\${CONFIG_DIR}/\${BASE_CONFIG}"
fi
if [[ ! -f "\$BASE_CONFIG" ]]; then
echo "ERROR: BASE_CONFIG file not found: \$BASE_CONFIG" >&2
exit 1
fi
if [[ ! "\$CASE_START" =~ ^[0-9]+\$ ]]; then
echo "ERROR: CASE_START must be numeric, got: \$CASE_START" >&2
exit 1
fi
if [[ "\$CASE_START" -lt 1000 ]]; then
echo "ERROR: CASE_START must be >= 1000 for consistent sorting, got: \$CASE_START" >&2
exit 1
fi
if [[ -n "\${CASE_END:-}" ]] && [[ ! "\$CASE_END" =~ ^[0-9]+\$ ]]; then
echo "ERROR: CASE_END must be numeric when provided, got: \$CASE_END" >&2
exit 1
fi
if [[ -n "\${CASE_END:-}" ]] && [[ "\$CASE_END" -lt 1000 ]]; then
echo "ERROR: CASE_END must be >= 1000 when provided, got: \$CASE_END" >&2
exit 1
fi
parse_sweep_variables "\$SWEEP_FILE"
TEMP_DIR="\$(mktemp -d "\${TMPDIR:-/tmp}/elastic-sweep.XXXXXX")"
trap 'rm -rf "\$TEMP_DIR"' EXIT
PARAM_FILES=()
COMBINATION_COUNT=0
CURRENT_CASE_NO="\$CASE_START"
generate_combinations 0
if [[ "\$COMBINATION_COUNT" -le 0 ]]; then
echo "ERROR: No parameter combinations generated." >&2
exit 1
fi
if [[ -n "\${CASE_END:-}" ]]; then
EXPECTED_COUNT=\$((CASE_END - CASE_START + 1))
if [[ "\$EXPECTED_COUNT" -ne "\$COMBINATION_COUNT" ]]; then
echo "ERROR: CASE_START/CASE_END imply \${EXPECTED_COUNT} cases, but generated \${COMBINATION_COUNT} combinations." >&2
exit 1
fi
else
CASE_END=\$((CASE_START + COMBINATION_COUNT - 1))
fi
echo "========================================="
echo "ElasticPinchOff - Parameter Sweep"
echo "========================================="
echo "Sweep file: \${SWEEP_FILE}"
echo "Source file: \${EXEC_CODE}"
echo "Mode: \${MODE}"
echo "Base config: \${BASE_CONFIG}"
echo "Sweep variables: \${#SWEEP_VARS[@]}"
echo "Cases: \${CASE_START}..\${CASE_END} (\${COMBINATION_COUNT})"
if [[ \$USE_MPI -eq 1 ]]; then
echo "Run mode: MPI (np=\${MPI_CPUS})"
else
echo "Run mode: Serial"
fi
if [[ \$DRY_RUN -eq 1 ]]; then
echo "Mode: Dry run"
fi
echo "========================================="
echo ""
if [[ \$DRY_RUN -eq 1 ]]; then
echo "Dry run complete. No simulations executed."
exit 0
fi
SUCCESSFUL=0
FAILED=0
for param_file in "\${PARAM_FILES[@]}"; do
case_no="\$(get_param_value "CaseNo" "\$param_file")"
ec_value="\$(get_param_value "Ec" "\$param_file")"
de_value="\$(get_param_value "De" "\$param_file")"
tmax_value="\$(get_param_value "tmax" "\$param_file")"
echo "-----------------------------------------"
echo "Running Case \${case_no}"
echo "Ec=\${ec_value:-NA}, De=\${de_value:-NA}, tmax=\${tmax_value:-NA}"
echo "Expected log file: c\${case_no}-log"
echo "-----------------------------------------"
run_cmd=(bash "\$RUN_SIM_SCRIPT" "\$param_file" --mode "\$MODE" --exec "\$EXEC_CODE")
if [[ \$USE_MPI -eq 1 ]]; then
run_cmd+=(--mpi --CPUs "\$MPI_CPUS")
fi
if "\${run_cmd[@]}"; then
((SUCCESSFUL += 1))
else
((FAILED += 1))
echo "ERROR: Case \${case_no} failed." >&2
fi
echo ""
done
echo "========================================="
echo "Parameter Sweep Complete"
echo "========================================="
echo "Total cases: \${COMBINATION_COUNT}"
echo "Successful: \${SUCCESSFUL}"
echo "Failed: \${FAILED}"
echo "Outputs: simulationCases/"
echo "========================================="
if [[ "\$FAILED" -gt 0 ]]; then
exit 1
fi
exit 0