#!/usr/bin/env bash
# Generate a Cartesian product of SWEEP_* values and route every case through
# the canonical single-case runner. Dry runs only parse configuration; they do
# not require Basilisk or a machine-local project configuration.
set -euo pipefail
SCRIPT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=src-local/parse_params.sh
source "\${SCRIPT_DIR}/src-local/parse_params.sh"
usage() {
command cat <<'EOF'
Usage: bash runParameterSweep.sh [sweep_file] [OPTIONS]
Options:
--exec FILE Simulation source (only hyphal-flow.c is supported)
--output-root DIR Case-output root passed to runSimulation.sh
--emit-params DIR Generate case parameter files without compiling or running
--dry-run Print generated cases without compiling or executing
--verbose Print all expanded parameter values
--mpi Compile and execute every case with MPI
--cpus N MPI process count (alias: --CPUs; default: 4)
-h, --help Show this help
EOF
}
require_value() {
if [[ \$# -lt 2 || -z "\${2:-}" ]]; then
printf 'ERROR: %s requires a value\n' "\$1" >&2
exit 2
fi
}
EXEC_CODE="hyphal-flow.c"
SWEEP_FILE="sweep.params"
SWEEP_FILE_SET=0
OUTPUT_ROOT="\${SCRIPT_DIR}/simulationCases"
EMIT_PARAMS=""
DRY_RUN=0
VERBOSE=0
USE_MPI=0
MPI_CPUS=4
while [[ \$# -gt 0 ]]; do
case "\$1" in
-h|--help) usage; exit 0 ;;
--exec) require_value "\$1" "\${2:-}"; EXEC_CODE="\$2"; shift 2 ;;
--exec=*) EXEC_CODE="\${1#*=}"; shift ;;
--output-root) require_value "\$1" "\${2:-}"; OUTPUT_ROOT="\$2"; shift 2 ;;
--output-root=*) OUTPUT_ROOT="\${1#*=}"; shift ;;
--emit-params) require_value "\$1" "\${2:-}"; EMIT_PARAMS="\$2"; shift 2 ;;
--emit-params=*) require_value "--emit-params" "\${1#*=}"; EMIT_PARAMS="\${1#*=}"; shift ;;
-n|--dry-run) DRY_RUN=1; shift ;;
-v|--verbose) VERBOSE=1; shift ;;
--mpi) USE_MPI=1; shift ;;
--cpus|--CPUs) require_value "\$1" "\${2:-}"; MPI_CPUS="\$2"; shift 2 ;;
--cpus=*|--CPUs=*) MPI_CPUS="\${1#*=}"; shift ;;
--) shift; break ;;
-*) printf 'ERROR: unknown option: %s\n' "\$1" >&2; usage; exit 2 ;;
*)
if [[ \$SWEEP_FILE_SET -ne 0 ]]; then
printf 'ERROR: unexpected argument: %s\n' "\$1" >&2
exit 2
fi
SWEEP_FILE="\$1"
SWEEP_FILE_SET=1
shift
;;
esac
done
if [[ \$# -gt 0 ]]; then
printf 'ERROR: unexpected trailing arguments: %s\n' "\$*" >&2
exit 2
fi
if [[ "\$EXEC_CODE" != "hyphal-flow.c" ]]; then
printf 'ERROR: only hyphal-flow.c is sweep-compatible; got %s\n' "\$EXEC_CODE" >&2
exit 2
fi
if [[ -z "\$OUTPUT_ROOT" ]]; then
printf 'ERROR: --output-root may not be empty\n' >&2
exit 2
fi
if [[ -n "\$EMIT_PARAMS" ]]; then
[[ "\$EMIT_PARAMS" = /* ]] || EMIT_PARAMS="\${SCRIPT_DIR}/\${EMIT_PARAMS}"
[[ ! -e "\$EMIT_PARAMS" ]] || {
printf 'ERROR: --emit-params target already exists: %s\n' "\$EMIT_PARAMS" >&2
exit 2
}
fi
if [[ ! "\$MPI_CPUS" =~ ^[1-9][0-9]*\$ ]]; then
printf 'ERROR: --cpus must be a positive integer; got %s\n' "\$MPI_CPUS" >&2
exit 2
fi
[[ "\$SWEEP_FILE" = /* ]] || SWEEP_FILE="\${SCRIPT_DIR}/\${SWEEP_FILE}"
[[ "\$OUTPUT_ROOT" = /* ]] || OUTPUT_ROOT="\${SCRIPT_DIR}/\${OUTPUT_ROOT}"
[[ -f "\$SWEEP_FILE" ]] || { printf 'ERROR: sweep file not found: %s\n' "\$SWEEP_FILE" >&2; exit 2; }
CONFIG_DIR="\$(cd "\$(dirname "\$SWEEP_FILE")" && pwd)"
BASE_CONFIG="\$(get_param_value BASE_CONFIG "\$SWEEP_FILE" default.params)"
CASE_START="\$(get_param_value CASE_START "\$SWEEP_FILE" 1000)"
CASE_END="\$(get_param_value CASE_END "\$SWEEP_FILE" '')"
[[ "\$BASE_CONFIG" = /* ]] || BASE_CONFIG="\${CONFIG_DIR}/\${BASE_CONFIG}"
[[ -f "\$BASE_CONFIG" ]] || { printf 'ERROR: base config not found: %s\n' "\$BASE_CONFIG" >&2; exit 2; }
if [[ ! "\$CASE_START" =~ ^[0-9]{4}\$ ]] || ((10#\$CASE_START < 1000 || 10#\$CASE_START > 9999)); then
printf 'ERROR: CASE_START must be four digits from 1000 to 9999; got %s\n' "\$CASE_START" >&2
exit 2
fi
if [[ -n "\$CASE_END" ]] && { [[ ! "\$CASE_END" =~ ^[0-9]{4}\$ ]] || ((10#\$CASE_END < 1000 || 10#\$CASE_END > 9999)); }; then
printf 'ERROR: CASE_END must be four digits from 1000 to 9999; got %s\n' "\$CASE_END" >&2
exit 2
fi
SWEEP_VARS=()
SWEEP_VALUES=()
while IFS= read -r line || [[ -n "\$line" ]]; do
line="\${line%%#*}"
line="\$(trim_string "\$line")"
[[ -z "\$line" || "\$line" != *=* ]] && continue
key="\$(trim_string "\${line%%=*}")"
value="\$(trim_string "\${line#*=}")"
if [[ "\$key" =~ ^SWEEP_([A-Za-z_][A-Za-z0-9_]*)\$ ]]; then
if [[ "\${BASH_REMATCH[1]}" == CaseNo ]]; then
printf 'ERROR: SWEEP_CaseNo is reserved for deterministic case numbering\n' >&2
exit 2
fi
[[ -n "\$value" ]] || { printf 'ERROR: %s has no values\n' "\$key" >&2; exit 2; }
SWEEP_VARS+=("\${BASH_REMATCH[1]}")
SWEEP_VALUES+=("\$value")
fi
done < "\$SWEEP_FILE"
[[ \${#SWEEP_VARS[@]} -gt 0 ]] || { printf 'ERROR: no SWEEP_* values in %s\n' "\$SWEEP_FILE" >&2; exit 2; }
TEMP_DIR="\$(mktemp -d "\${TMPDIR:-/tmp}/hyphal-sweep.XXXXXX")"
trap 'rm -rf "\$TEMP_DIR"' EXIT
PARAM_FILES=()
COMBINATION_COUNT=0
CURRENT_CASE=\$((10#\$CASE_START))
generate_combinations() {
local depth="\$1"
shift
local current=("\$@")
local index raw value case_file
local values=()
if [[ "\$depth" -eq "\${#SWEEP_VARS[@]}" ]]; then
if ((CURRENT_CASE > 9999)); then
printf 'ERROR: generated CaseNo exceeds 9999\n' >&2
exit 2
fi
case_file="\${TEMP_DIR}/case_\$(printf '%04d' "\$CURRENT_CASE").params"
cp "\$BASE_CONFIG" "\$case_file"
set_param_in_file CaseNo "\$(printf '%04d' "\$CURRENT_CASE")" "\$case_file"
for index in "\${!SWEEP_VARS[@]}"; do
set_param_in_file "\${SWEEP_VARS[\$index]}" "\${current[\$index]}" "\$case_file"
done
PARAM_FILES+=("\$case_file")
if [[ \$DRY_RUN -eq 1 || \$VERBOSE -eq 1 ]]; then
printf 'Case %04d:' "\$CURRENT_CASE"
for index in "\${!SWEEP_VARS[@]}"; do
printf ' %s=%s' "\${SWEEP_VARS[\$index]}" "\${current[\$index]}"
done
printf '\n'
fi
((COMBINATION_COUNT += 1))
((CURRENT_CASE += 1))
return
fi
IFS=',' read -r -a values <<< "\${SWEEP_VALUES[\$depth]}"
for raw in "\${values[@]}"; do
value="\$(trim_string "\$raw")"
[[ -n "\$value" ]] || continue
if [[ \${#current[@]} -gt 0 ]]; then
generate_combinations "\$((depth + 1))" "\${current[@]}" "\$value"
else
generate_combinations "\$((depth + 1))" "\$value"
fi
done
}
generate_combinations 0
((COMBINATION_COUNT > 0)) || { printf 'ERROR: no combinations generated\n' >&2; exit 2; }
GENERATED_END=\$((10#\$CASE_START + COMBINATION_COUNT - 1))
if [[ -n "\$CASE_END" ]] && ((10#\$CASE_END != GENERATED_END)); then
printf 'ERROR: CASE_START/CASE_END imply %d cases but %d combinations were generated\n' \
"\$((10#\$CASE_END - 10#\$CASE_START + 1))" "\$COMBINATION_COUNT" >&2
exit 2
fi
printf 'Sweep: %d case(s), %04d..%04d, source=%s\n' \
"\$COMBINATION_COUNT" "\$((10#\$CASE_START))" "\$GENERATED_END" "\$EXEC_CODE"
if [[ -n "\$EMIT_PARAMS" ]]; then
mkdir "\$EMIT_PARAMS"
for param_file in "\${PARAM_FILES[@]}"; do
case_number="\${param_file##*case_}"
case_number="\${case_number%.params}"
cp "\$param_file" "\${EMIT_PARAMS}/case-\${case_number}.params"
done
printf 'Wrote %d case parameter files to %s\n' \
"\$COMBINATION_COUNT" "\$EMIT_PARAMS"
exit 0
fi
if [[ \$DRY_RUN -eq 1 ]]; then
printf 'Dry run complete; no simulations executed.\n'
exit 0
fi
SUCCESSFUL=0
FAILED=0
for param_file in "\${PARAM_FILES[@]}"; do
command=(bash "\${SCRIPT_DIR}/runSimulation.sh" "\$param_file"
--exec "\$EXEC_CODE" --output-root "\$OUTPUT_ROOT")
if [[ \$USE_MPI -eq 1 ]]; then
command+=(--mpi --cpus "\$MPI_CPUS")
fi
if "\${command[@]}"; then
((SUCCESSFUL += 1))
else
((FAILED += 1))
fi
done
printf 'Sweep complete: %d passed, %d failed.\n' "\$SUCCESSFUL" "\$FAILED"
((FAILED == 0))