Bootstrap: docker From: debian:trixie %labels Author ATGC %post set -eu export DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y --no-install-recommends \ build-essential wget git autoconf automake libtool \ bc dos2unix r-base-core ca-certificates unzip python3 cd /opt wget https://github.com/stephaneguindon/phyml/archive/refs/tags/v3.3.20250515.tar.gz echo "c8d09f52f080a95dde8d7d797da5874796a148158cd18f956110cdba13cd3368 v3.3.20250515.tar.gz" | sha256sum -c - tar xvf v3.3.20250515.tar.gz cd phyml-3.3.20250515 sh ./autogen.sh && ./configure && make clean && make V=1 cp src/phyml /usr/local/bin/ cd /opt git clone https://gite.lirmm.fr/atgc/sms.git cd sms git checkout --detach 916269c4bfa1374fe94117dfdf6e8fb7b3d67791 ZIP_FILE="$(find tarball -maxdepth 1 -name 'sms-*.zip' -print -quit)" if [ -z "${ZIP_FILE}" ]; then echo "Unable to locate SMS tarball in sms/tarball" >&2 exit 1 fi unzip -q "${ZIP_FILE}" ZIP_DIR="$(basename "${ZIP_FILE}" .zip)" rm -rf sources/phyml-src mv "${ZIP_DIR}/phyml-src" sources/ rm -rf "${ZIP_DIR}" cd sources # Modern R rejects order(data.frame); order the numeric distance column. grep -Fq 'row.names(d)[order(d)]' chi.R sed -i 's/order(d)/order(d[[1]])/' chi.R # Sort numeric scores before converting decimal points for the CSV export. sed -i -E "s/tr '\\.' ',' \\| sort -g -k ([78])/LC_ALL=C sort -g -k \\1 | tr '.' ','/g" sms.sh # Localize numeric fields only: model names such as Q.yeast are identifiers. python3 - <<'PYEXPORT' from pathlib import Path path = Path("sms.sh") source = path.read_text() old = "tr '.' ','" assert source.count(old) == 4, "Unexpected SMS CSV export layout" formatter = r"""awk 'BEGIN {FS=OFS="\t"} {for(i=1;i<=NF;i++) if($i ~ /^-?[0-9]+[.][0-9]+$/) gsub(/[.]/, ",", $i); print}'""" path.write_text(source.replace(old, formatter)) PYEXPORT make sed -i 's|^PHYMLBIN=.*|PHYMLBIN="/usr/local/bin/phyml"|' sms.cfg chmod +x *.sh cat <<'EOF' >/usr/local/bin/phyml-wrapper #!/bin/bash set -euo pipefail SMS_ROOT="/opt/sms/sources" SMS_GAMMA_CATEGORIES=4 SMS_FREE_RATE_CATEGORIES=4 PHYML_BIN="phyml" SMS_MODE=0 SMS_CRITERION="" declare -a PHYML_ARGS=() while [[ $# -gt 0 ]]; do case "$1" in --sms) SMS_MODE=1 shift if [[ $# -gt 0 ]]; then SMS_CRITERION="$(echo "$1" | tr '[:lower:]' '[:upper:]')" shift fi ;; *) PHYML_ARGS+=("$1") shift ;; esac done if [[ ${SMS_MODE} -eq 0 ]]; then exec "${PHYML_BIN}" "${PHYML_ARGS[@]}" fi if [[ -z "${SMS_CRITERION}" ]]; then SMS_CRITERION="BIC" fi INPUT_FILE="" DATA_TYPE="" for (( idx=0; idx<${#PHYML_ARGS[@]}; idx++ )); do arg="${PHYML_ARGS[$idx]}" case "${arg}" in -i) if (( idx + 1 < ${#PHYML_ARGS[@]} )); then INPUT_FILE="${PHYML_ARGS[$((idx + 1))]}" fi ;; -d) if (( idx + 1 < ${#PHYML_ARGS[@]} )); then DATA_TYPE="${PHYML_ARGS[$((idx + 1))]}" fi ;; esac done if [[ -z "${INPUT_FILE}" || -z "${DATA_TYPE}" ]]; then echo "SMS wrapper error: missing alignment (-i) or data type (-d)." >&2 exit 1 fi SMS_DIR="sms_workspace" SMS_CSV="${SMS_DIR}/sms_results.csv" rm -rf "${SMS_DIR}" mkdir -p "${SMS_DIR}" # Operator-only recovery: reuse this job's saved ranking after a wrapper failure. # Normal submissions always run SMS. The saved ranking is validated below too. if [[ -n "${PHYML_SMS_RESULTS:-}" ]]; then cp -- "${PHYML_SMS_RESULTS}" "${SMS_CSV}" echo "[phyml-wrapper] Reusing the job's recorded SMS ranking (criterion=${SMS_CRITERION})." >&2 else echo "[phyml-wrapper] Running SMS (criterion=${SMS_CRITERION})" >&2 if ! "${SMS_ROOT}/sms.sh" -i "${INPUT_FILE}" -d "${DATA_TYPE}" -c "${SMS_CRITERION}" -o "${SMS_DIR}" -p "${SMS_CSV}"; then echo "SMS execution failed." >&2 exit 1 fi fi if [[ ! -f "${SMS_CSV}" ]]; then echo "SMS results file not found at ${SMS_CSV}" >&2 exit 1 fi cp "${SMS_CSV}" "sms_results.csv" if [[ -f "${SMS_DIR}/sms.log" ]]; then cp "${SMS_DIR}/sms.log" "sms.log" fi # SMS can exit zero after a failed matrix selection. Never infer a tree from # the remaining named rows of an incomplete ranking. if ! awk -F';' ' NR == 1 { if ($1 != "Model" || $2 != "Decoration") invalid=1; next } NF > 1 { rows++; if ($1 ~ /^[[:space:]]*$/ || NF < 6) invalid=1 } END { exit (invalid || rows == 0) } ' "${SMS_CSV}"; then echo "SMS model selection failed: incomplete model ranking. PhyML inference was not started." >&2 exit 1 fi BEST_LINE="$(awk -F';' 'NR > 1 && NF > 1 { print; exit }' "${SMS_CSV}")" BEST_MODEL="$(echo "${BEST_LINE}" | cut -d ';' -f 1 | tr -d '[:space:]\r')" # Older SMS exports localized dots even in model identifiers. BEST_MODEL="${BEST_MODEL//,/.}" BEST_DECORATION_RAW="$(echo "${BEST_LINE}" | cut -d ';' -f 2 | tr -d '\r')" BEST_DECORATION="$(echo "${BEST_DECORATION_RAW}" | tr -d ' \"')" if [[ -z "${BEST_MODEL}" ]]; then echo "Unable to parse a non-empty best model from SMS results." >&2 exit 1 fi echo "[phyml-wrapper] SMS selected model: ${BEST_MODEL} ${BEST_DECORATION}" >&2 declare -a SMS_MODEL_ARGS=() case "${BEST_MODEL}" in HKY85|JC69|K80|K2P|F81|F84|TN93|GTR) SMS_MODEL_ARGS+=("-m" "${BEST_MODEL}") ;; SYM) SMS_MODEL_ARGS+=("-m" "012345") ;; *) MATRIX_PATH="${SMS_ROOT}/matrix/${BEST_MODEL}.mt" if [[ -f "${MATRIX_PATH}" ]]; then SMS_MODEL_ARGS+=("-m" "custom" "--aa_rate_file" "${MATRIX_PATH}") else SMS_MODEL_ARGS+=("-m" "${BEST_MODEL}") fi ;; esac declare -a SMS_FREQ_ARGS=() if [[ "${DATA_TYPE}" == "nt" ]]; then SMS_FREQ_ARGS+=("-f" "e") else if [[ "${BEST_DECORATION}" == *"+F"* ]]; then SMS_FREQ_ARGS+=("-f" "e") else SMS_FREQ_ARGS+=("-f" "m") fi fi DECO_SANITIZED="${BEST_DECORATION//\'/}" DECO_SANITIZED="${DECO_SANITIZED// /}" DECO_SANITIZED="${DECO_SANITIZED//$'\r'/}" DECO_SANITIZED="${DECO_SANITIZED//$'\n'/}" DECO_SANITIZED="${DECO_SANITIZED//+F/}" declare -a SMS_VAR_ARGS=() case "${DECO_SANITIZED}" in "") ;; "+G") SMS_VAR_ARGS+=("-c" "${SMS_GAMMA_CATEGORIES}" "-a" "e") ;; "+I") SMS_VAR_ARGS+=("-v" "e") ;; "+R") SMS_VAR_ARGS+=("--freerates" "-c" "${SMS_FREE_RATE_CATEGORIES}") ;; "+G+I"|"+I+G") SMS_VAR_ARGS+=("-c" "${SMS_GAMMA_CATEGORIES}" "-a" "e" "-v" "e") ;; "+R+I"|"+I+R") SMS_VAR_ARGS+=("--freerates" "-c" "${SMS_FREE_RATE_CATEGORIES}" "-v" "e") ;; *) echo "[phyml-wrapper] Warning: unhandled SMS decoration '${DECO_SANITIZED}', proceeding without extra rate options." >&2 ;; esac FINAL_ARGS=("${PHYML_ARGS[@]}" "${SMS_MODEL_ARGS[@]}" "${SMS_FREQ_ARGS[@]}" "${SMS_VAR_ARGS[@]}") exec "${PHYML_BIN}" "${FINAL_ARGS[@]}" EOF chmod +x /usr/local/bin/phyml-wrapper apt-get clean && rm -rf /var/lib/apt/lists/* %environment export PATH=/usr/local/bin:$PATH %runscript exec /usr/local/bin/phyml-wrapper "$@"