#!/usr/bin/env bash
# Compile and run one canonical hyphal-flow case. Runtime output is isolated
# from source; an existing case is never reused without an explicit,
# input-matched --resume.
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 runSimulation.sh [params_file] [OPTIONS]
Options:
--exec FILE Simulation source (only hyphal-flow.c is supported)
--output-root DIR Case-output root (default: simulationCases)
--compile-only Compile the case but do not execute it
--dry-run Validate and print the intended compile/run without writes
--resume Resume only when source and parameter hashes match
--mpi Compile and execute 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"
PARAM_FILE="default.params"
PARAM_FILE_SET=0
OUTPUT_ROOT="\${SCRIPT_DIR}/simulationCases"
COMPILE_ONLY=0
DRY_RUN=0
RESUME=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 ;;
--compile-only) COMPILE_ONLY=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--resume) RESUME=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 [[ \$PARAM_FILE_SET -ne 0 ]]; then
printf 'ERROR: unexpected argument: %s\n' "\$1" >&2
exit 2
fi
PARAM_FILE="\$1"
PARAM_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 simulationCases/hyphal-flow.c accepts the runtime contract; 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 [[ ! "\$MPI_CPUS" =~ ^[1-9][0-9]*\$ ]]; then
printf 'ERROR: --cpus must be a positive integer; got %s\n' "\$MPI_CPUS" >&2
exit 2
fi
[[ "\$PARAM_FILE" = /* ]] || PARAM_FILE="\${SCRIPT_DIR}/\${PARAM_FILE}"
[[ "\$OUTPUT_ROOT" = /* ]] || OUTPUT_ROOT="\${SCRIPT_DIR}/\${OUTPUT_ROOT}"
SOURCE_FILE="\${SCRIPT_DIR}/simulationCases/\${EXEC_CODE}"
[[ -f "\$PARAM_FILE" ]] || { printf 'ERROR: parameter file not found: %s\n' "\$PARAM_FILE" >&2; exit 2; }
[[ -f "\$SOURCE_FILE" ]] || { printf 'ERROR: source file not found: %s\n' "\$SOURCE_FILE" >&2; exit 2; }
CASE_NO="\$(get_param_value CaseNo "\$PARAM_FILE")"
if [[ ! "\$CASE_NO" =~ ^[0-9]{4}\$ ]] || ((10#\$CASE_NO < 1000 || 10#\$CASE_NO > 9999)); then
printf 'ERROR: CaseNo must be a four-digit value from 1000 to 9999; got %s\n' "\${CASE_NO:-<missing>}" >&2
exit 2
fi
# A local configuration may add qcc to PATH, but a clean clone does not need it.
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
QCC="\$(command -v qcc)"
elif [[ -n "\${BASILISK:-}" && -x "\${BASILISK}/qcc" ]]; then
QCC="\${BASILISK}/qcc"
else
printf 'ERROR: qcc is not available in PATH or BASILISK/qcc\n' >&2
exit 2
fi
if [[ -n "\${BASILISK:-}" && -f "\${BASILISK}/grid/multigrid-common.h" ]]; then
BASILISK_API_HEADER="\${BASILISK}/grid/multigrid-common.h"
elif [[ -n "\${BASILISK:-}" && -f "\${BASILISK}/src/grid/multigrid-common.h" ]]; then
BASILISK_API_HEADER="\${BASILISK}/src/grid/multigrid-common.h"
else
QCC_DIR="\$(dirname "\$QCC")"
BASILISK_API_HEADER="\${QCC_DIR}/grid/multigrid-common.h"
fi
[[ -f "\$BASILISK_API_HEADER" ]] || {
printf 'ERROR: cannot locate Basilisk grid headers; set BASILISK to the source tree\n' >&2
exit 2
}
if grep -q 'void set_prolongation' "\$BASILISK_API_HEADER"; then
BASILISK_API=modern
else
BASILISK_API=legacy
fi
if [[ \$USE_MPI -eq 1 ]]; then
command -v mpicc >/dev/null 2>&1 || { printf 'ERROR: mpicc is required for --mpi\n' >&2; exit 2; }
command -v mpirun >/dev/null 2>&1 || { printf 'ERROR: mpirun is required for --mpi\n' >&2; exit 2; }
fi
command -v shasum >/dev/null 2>&1 || { printf 'ERROR: shasum is required for input manifests\n' >&2; exit 2; }
CASE_DIR="\${OUTPUT_ROOT}/\${CASE_NO}"
EXECUTABLE="\${CASE_DIR}/hyphal-flow"
SOURCE_HASH="\$(shasum -a 256 "\$SOURCE_FILE" "\${SCRIPT_DIR}"/src-local/*.h \
"\${SCRIPT_DIR}/runSimulation.sh" | awk '{print \$1}' | shasum -a 256 | awk '{print \$1}')"
SOURCE_FILE_HASH="\$(shasum -a 256 "\$SOURCE_FILE" | awk '{print \$1}')"
PARAM_HASH="\$(shasum -a 256 "\$PARAM_FILE" | awk '{print \$1}')"
MANIFEST="\${CASE_DIR}/run-manifest.txt"
if [[ \$USE_MPI -eq 1 ]]; then
RUN_MODE=mpi
MANIFEST_CPUS="\$MPI_CPUS"
else
RUN_MODE=serial
MANIFEST_CPUS=1
fi
manifest_matches() {
[[ -f "\$MANIFEST" && -f "\${CASE_DIR}/\${EXEC_CODE}" &&
-f "\${CASE_DIR}/case.params" ]] || return 1
grep -Fxq "source_sha256=\${SOURCE_HASH}" "\$MANIFEST" &&
grep -Fxq "source_file_sha256=\${SOURCE_FILE_HASH}" "\$MANIFEST" &&
grep -Fxq "params_sha256=\${PARAM_HASH}" "\$MANIFEST" &&
grep -Fxq "mode=\${RUN_MODE}" "\$MANIFEST" &&
grep -Fxq "cpus=\${MANIFEST_CPUS}" "\$MANIFEST" &&
grep -Fxq "basilisk_api=\${BASILISK_API}" "\$MANIFEST" &&
[[ "\$(shasum -a 256 "\${CASE_DIR}/\${EXEC_CODE}" | awk '{print \$1}')" == "\$SOURCE_FILE_HASH" ]] &&
[[ "\$(shasum -a 256 "\${CASE_DIR}/case.params" | awk '{print \$1}')" == "\$PARAM_HASH" ]]
}
compile_only_case() {
[[ ! -e "\${CASE_DIR}/restart" && ! -e "\${CASE_DIR}/final" &&
! -e "\${CASE_DIR}/log" && ! -d "\${CASE_DIR}/intermediate" ]] || return 1
[[ -z "\$(find "\$CASE_DIR" -mindepth 1 -maxdepth 1 \
! -name case.params ! -name "\$EXEC_CODE" ! -name run-manifest.txt \
! -name hyphal-flow -print -quit)" ]]
}
printf 'Case %s: %s\n' "\$CASE_NO" "\$CASE_DIR"
printf 'Source: %s\n' "\$SOURCE_FILE"
printf 'Parameters: %s\n' "\$PARAM_FILE"
printf 'Mode: %s\n' "\$([[ \$USE_MPI -eq 1 ]] && printf 'MPI (%s ranks)' "\$MPI_CPUS" || printf 'serial')"
if [[ \$DRY_RUN -eq 1 ]]; then
printf 'Dry run: would compile with %s and %s the case.\n' "\$QCC" "\$([[ \$COMPILE_ONLY -eq 1 ]] && printf 'not execute' || printf 'execute')"
exit 0
fi
if [[ -d "\$CASE_DIR" && -n "\$(find "\$CASE_DIR" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then
if [[ \$RESUME -eq 1 ]]; then
[[ -f "\${CASE_DIR}/restart" ]] || {
printf 'ERROR: --resume requires run-manifest.txt and restart\n' >&2
exit 2
}
manifest_matches || {
printf 'ERROR: --resume inputs differ from the recorded case manifest\n' >&2
exit 2
}
elif manifest_matches && compile_only_case; then
printf 'Reusing exact compile-only case inputs: %s\n' "\$CASE_DIR"
else
printf 'ERROR: case directory is non-empty; use a new CaseNo or an input-matched --resume: %s\n' "\$CASE_DIR" >&2
exit 2
fi
else
mkdir -p "\$CASE_DIR"
cp "\$PARAM_FILE" "\${CASE_DIR}/case.params"
cp "\$SOURCE_FILE" "\${CASE_DIR}/\${EXEC_CODE}"
{
printf 'source_sha256=%s\n' "\$SOURCE_HASH"
printf 'source_file_sha256=%s\n' "\$SOURCE_FILE_HASH"
printf 'params_sha256=%s\n' "\$PARAM_HASH"
printf 'source=%s\n' "\$EXEC_CODE"
printf 'case=%s\n' "\$CASE_NO"
printf 'mode=%s\n' "\$RUN_MODE"
printf 'cpus=%s\n' "\$MANIFEST_CPUS"
printf 'basilisk_api=%s\n' "\$BASILISK_API"
} > "\$MANIFEST"
fi
LOG_LINES_BEFORE=0
LAST_TIME_BEFORE=-1
if [[ \$RESUME -eq 1 && -f "\${CASE_DIR}/log" ]]; then
LOG_LINES_BEFORE="\$(wc -l < "\${CASE_DIR}/log")"
LAST_TIME_BEFORE="\$(awk 'NR > 1 { value=\$3 } END { print value + 0 }' "\${CASE_DIR}/log")"
fi
cd "\$CASE_DIR"
compile=("\$QCC" "-I\${SCRIPT_DIR}/src-local" -Wall -O2 -disable-dimensions)
if [[ "\$BASILISK_API" == legacy ]]; then
compile+=( -DHYPHAL_LEGACY_BASILISK=1 )
fi
compile+=( "\$EXEC_CODE" -o hyphal-flow -lm )
printf 'Compiling %s\n' "\$EXEC_CODE"
if [[ \$USE_MPI -eq 1 ]]; then
MPI_CC99='mpicc -std=c99'
if [[ "\$(uname -s)" != Darwin ]]; then
MPI_CC99+=' -D_GNU_SOURCE=1'
fi
CC99="\$MPI_CC99" "\${compile[@]:0:3}" -D_MPI=1 "\${compile[@]:3}"
else
"\${compile[@]}"
fi
if [[ \$COMPILE_ONLY -eq 1 ]]; then
printf 'Compile succeeded: %s\n' "\$EXECUTABLE"
exit 0
fi
if [[ \$USE_MPI -eq 1 ]]; then
run_command=(mpirun -np "\$MPI_CPUS" ./hyphal-flow case.params)
else
run_command=(./hyphal-flow case.params)
fi
printf 'Running case %s\n' "\$CASE_NO"
"\${run_command[@]}"
[[ -s log && -s restart && -s final ]] || {
printf 'ERROR: run exited without non-empty log, restart and final outputs\n' >&2
exit 1
}
if [[ \$RESUME -eq 1 ]]; then
LOG_LINES_AFTER="\$(wc -l < log)"
LAST_TIME_AFTER="\$(awk 'NR > 1 { value=\$3 } END { print value + 0 }' log)"
if ((LOG_LINES_AFTER <= LOG_LINES_BEFORE)) ||
! awk -v before="\$LAST_TIME_BEFORE" -v after="\$LAST_TIME_AFTER" \
'BEGIN { exit(after > before ? 0 : 1) }'; then
printf 'ERROR: resumed simulation produced no forward log progress\n' >&2
exit 1
fi
fi
awk 'NR > 1 && \$3 > 0 { found=1 } END { exit(found ? 0 : 1) }' log || {
printf 'ERROR: log contains no positive simulation time\n' >&2
exit 1
}
if grep -Ein '(^|[^[:alnum:]_])(nan|[-+]?inf(inity)?)([^[:alnum:]_]|\$)|ERROR: non-' log >/dev/null 2>&1; then
printf 'ERROR: numerical failure signature found in log\n' >&2
exit 1
fi
printf 'Simulation completed: %s\n' "\$CASE_DIR"