Skip to content

Commit a8f4672

Browse files
Unit A: borrowed external anatomical T1 reference (ANATOMICAL_REFERENCE_T1) (#146)
Adopt an operator-supplied external non-contrast T1 as the anatomical anchor when a study has no usable in-study T1: copied into the extract dir (checked, idempotent), provenance recorded once, PIPELINE_REFERENCE_MODALITY set to T1 so it is the fixed anchor, and the adopted copy is the recon-all input. Fires whether reference selection found a FLAIR or failed entirely. Fully backward-compatible: default empty = unchanged behavior. Generic only; subject paths are operator config. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ad907ca commit a8f4672

2 files changed

Lines changed: 151 additions & 26 deletions

File tree

config/default_config.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,29 @@ export OUTPUT_DATATYPE="int" # final int16
7373
# Atlas and template configuration
7474
export DEFAULT_TEMPLATE_RES="${DEFAULT_TEMPLATE_RES:-1mm}"
7575

76+
# ---------------------------------------------------------------------------
77+
# External anatomical T1 reference (Unit A — borrowed/external anchor)
78+
# ---------------------------------------------------------------------------
79+
# Allows an external non-contrast T1 (from another acquisition of the same
80+
# subject) to serve as the anatomical anchor when the current study has NO
81+
# usable in-study T1. Default empty = current behaviour (unaffected).
82+
#
83+
# ANATOMICAL_REFERENCE_T1 : absolute path to an external full-head
84+
# non-contrast T1 NIfTI. When non-empty AND
85+
# the file exists, and no in-study T1 is found,
86+
# the pipeline ADOPTS it (copies into EXTRACT_DIR,
87+
# records provenance) instead of aborting.
88+
# Subject-specific paths must never be committed;
89+
# supply via operator config or environment only.
90+
# ANATOMICAL_REFERENCE_LABEL : free-text provenance label recorded in outputs
91+
# (e.g. session name or acquisition date).
92+
# PREFER_EXTERNAL_NONCONTRAST_T1 : (reserved for Unit C — contrast-T1
93+
# detection; declared here so config files that
94+
# reference it are forward-compatible).
95+
export ANATOMICAL_REFERENCE_T1="${ANATOMICAL_REFERENCE_T1:-}"
96+
export ANATOMICAL_REFERENCE_LABEL="${ANATOMICAL_REFERENCE_LABEL:-}"
97+
export PREFER_EXTERNAL_NONCONTRAST_T1="${PREFER_EXTERNAL_NONCONTRAST_T1:-false}"
98+
7699
# ---------------------------------------------------------------------------
77100
# Brainstem segmentation method
78101
# ---------------------------------------------------------------------------

src/pipeline.sh

Lines changed: 128 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -501,39 +501,137 @@ run_pipeline() {
501501
log_message "Selected file: $(basename "$selected_file")"
502502
log_message "Rationale: $selection_rationale"
503503

504-
# Assign files based on selection result
505-
if [ "$selected_modality" == "T1" ]; then
506-
local t1_file="$selected_file"
504+
# ---------------------------------------------------------------------------
505+
# Assign files based on selection result, with external anatomical T1
506+
# adoption (Unit A) when no in-study T1 is available.
507+
#
508+
# Resolution order (executed once; ANATOMICAL_REFERENCE_T1 is considered
509+
# only when the in-study T1 is absent or reference selection failed):
510+
#
511+
# 1. Reference selection chose T1 → use it directly.
512+
# 2. Reference selection chose FLAIR → look for an in-study T1;
513+
# if none found and ANATOMICAL_REFERENCE_T1 is set, adopt it (below).
514+
# 3. Reference selection failed entirely → if ANATOMICAL_REFERENCE_T1
515+
# is set, adopt it and find the best FLAIR in EXTRACT_DIR; else abort.
516+
#
517+
# When ANATOMICAL_REFERENCE_T1 is unset/empty (the default), none of the
518+
# adoption code below is reachable — behaviour is byte-identical to before.
519+
# ---------------------------------------------------------------------------
520+
local t1_file="" flair_file=""
521+
# Tracks whether the external anatomical T1 adoption fired (used when setting
522+
# PIPELINE_REFERENCE_MODALITY below so we don't confuse "T1 selected by
523+
# selection" with "T1 from adoption").
524+
local _external_t1_adopted="false"
525+
526+
if [ "$selected_modality" = "T1" ]; then
527+
t1_file="$selected_file"
507528
# Find the best FLAIR for this T1
508-
local flair_file=$(select_best_scan "SPACE_FLAIR" "*SPACE_FLAIR*.nii.gz" "$EXTRACT_DIR" "$t1_file" "${FLAIR_SELECTION_MODE:-registration_optimized}")
509-
elif [ "$selected_modality" == "FLAIR" ]; then
510-
local flair_file="$selected_file"
511-
# Find the best T1 for this FLAIR
512-
local t1_file=$(select_best_scan "T1" "*T1*.nii.gz" "$EXTRACT_DIR" "$flair_file" "${T1_SELECTION_MODE:-highest_resolution}")
529+
flair_file=$(select_best_scan "SPACE_FLAIR" "*SPACE_FLAIR*.nii.gz" "$EXTRACT_DIR" "$t1_file" "${FLAIR_SELECTION_MODE:-registration_optimized}")
530+
elif [ "$selected_modality" = "FLAIR" ]; then
531+
flair_file="$selected_file"
532+
# Find the best in-study T1 for this FLAIR
533+
t1_file=$(select_best_scan "T1" "*T1*.nii.gz" "$EXTRACT_DIR" "$flair_file" "${T1_SELECTION_MODE:-highest_resolution}")
534+
# If no in-study T1, fall through to the adoption block below.
513535
else
514-
log_error "Reference space selection failed: $selection_rationale" $ERR_DATA_MISSING
515-
return $ERR_DATA_MISSING
536+
# Reference selection returned neither T1 nor FLAIR (or failed). If
537+
# ANATOMICAL_REFERENCE_T1 is supplied we can still proceed; otherwise abort.
538+
if [ -z "${ANATOMICAL_REFERENCE_T1:-}" ]; then
539+
log_error "Reference space selection failed and ANATOMICAL_REFERENCE_T1 is not set: $selection_rationale" $ERR_DATA_MISSING
540+
return $ERR_DATA_MISSING
541+
fi
542+
log_formatted "WARNING" "Reference space selection found no usable reference: $selection_rationale — will attempt adoption of ANATOMICAL_REFERENCE_T1"
543+
# t1_file remains empty; the adoption block below sets it.
544+
# Discover FLAIR independently (no T1 anchor available yet).
545+
flair_file=$(select_best_scan "SPACE_FLAIR" "*SPACE_FLAIR*.nii.gz" "$EXTRACT_DIR" "" "${FLAIR_SELECTION_MODE:-registration_optimized}")
516546
fi
517-
518-
# Set global variables to track the authoritative reference space decision
519-
if [ "$selected_modality" == "T1" ]; then
547+
548+
# ---------------------------------------------------------------------------
549+
# External anatomical T1 adoption (Unit A — borrowed/external anchor)
550+
# ---------------------------------------------------------------------------
551+
# Fires when no in-study T1 was found AND ANATOMICAL_REFERENCE_T1 is a
552+
# non-empty path to an existing file.
553+
# The external T1 is COPIED into EXTRACT_DIR under a fixed name so that all
554+
# downstream discovery treats it like a normal in-study T1.
555+
# The copy is refreshed when ANATOMICAL_REFERENCE_T1 changes between runs
556+
# (detected by byte-size mismatch against the destination), so a changed
557+
# external reference is never silently ignored on resume.
558+
# Provenance is written once per run directory (overwrite = idempotent).
559+
# When ANATOMICAL_REFERENCE_T1 is unset/empty this block is a complete no-op.
560+
# ---------------------------------------------------------------------------
561+
if [ -z "${t1_file:-}" ] && [ -n "${ANATOMICAL_REFERENCE_T1:-}" ]; then
562+
if [ ! -f "${ANATOMICAL_REFERENCE_T1}" ]; then
563+
log_error "ANATOMICAL_REFERENCE_T1 set but file not found: ${ANATOMICAL_REFERENCE_T1}" $ERR_DATA_MISSING
564+
return $ERR_DATA_MISSING
565+
fi
566+
# Require .nii.gz input — raw .nii would be copied byte-for-byte into a
567+
# .nii.gz-named destination, producing an invalid compressed file that FSL
568+
# and NIfTI readers would reject or misparse.
569+
if [[ "${ANATOMICAL_REFERENCE_T1}" != *.nii.gz ]]; then
570+
log_error "ANATOMICAL_REFERENCE_T1 must be a compressed NIfTI (.nii.gz); got: ${ANATOMICAL_REFERENCE_T1}" $ERR_DATA_MISSING
571+
return $ERR_DATA_MISSING
572+
fi
573+
local adopted_t1="${EXTRACT_DIR}/external_anatomical_T1.nii.gz"
574+
local ref_label="${ANATOMICAL_REFERENCE_LABEL:-${ANATOMICAL_REFERENCE_T1}}"
575+
log_formatted "WARNING" "No in-study T1 found; adopting EXTERNAL anatomical T1 reference: ${ref_label}"
576+
# Refresh the copy when source and destination sizes differ (covers both the
577+
# first run and a changed ANATOMICAL_REFERENCE_T1 on a subsequent resume).
578+
local _src_size _dst_size
579+
_src_size=$(wc -c < "${ANATOMICAL_REFERENCE_T1}" 2>/dev/null || echo 0)
580+
_dst_size=$([ -f "${adopted_t1}" ] && wc -c < "${adopted_t1}" 2>/dev/null || echo 0)
581+
if [ "${_src_size}" != "${_dst_size}" ] || [ ! -f "${adopted_t1}" ]; then
582+
log_message "Copying external anatomical T1 into extract dir (source size: ${_src_size} bytes)"
583+
if ! cp "${ANATOMICAL_REFERENCE_T1}" "${adopted_t1}"; then
584+
log_error "Failed to copy external anatomical T1 reference into EXTRACT_DIR" $ERR_DATA_MISSING
585+
return $ERR_DATA_MISSING
586+
fi
587+
else
588+
log_message "External anatomical T1 already present in extract dir (size matches; skipping copy)"
589+
fi
590+
# Sanity-check the adopted file: must exist and be non-empty.
591+
local _adopted_size
592+
_adopted_size=$(wc -c < "${adopted_t1}" 2>/dev/null || echo 0)
593+
if [ ! -f "${adopted_t1}" ] || [ "${_adopted_size}" -eq 0 ]; then
594+
log_error "Adopted external anatomical T1 is missing or empty: ${adopted_t1}" $ERR_DATA_MISSING
595+
return $ERR_DATA_MISSING
596+
fi
597+
t1_file="${adopted_t1}"
598+
_external_t1_adopted="true"
599+
# Record provenance (overwrite = idempotent across resumes).
600+
printf 'source=%s\nlabel=%s\ntype=external\nadopted_as=%s\n' \
601+
"${ANATOMICAL_REFERENCE_T1}" "${ref_label}" "${adopted_t1}" \
602+
> "${RESULTS_DIR}/anatomical_reference_provenance.txt"
603+
log_message "External anatomical T1 adopted as: ${adopted_t1}"
604+
log_message "Provenance recorded in: ${RESULTS_DIR}/anatomical_reference_provenance.txt"
605+
fi
606+
# ---------------------------------------------------------------------------
607+
608+
# Set global variables to track the authoritative reference space decision.
609+
# The external T1 is always the fixed anatomical anchor when adoption fired
610+
# (PIPELINE_REFERENCE_MODALITY=T1, FLAIR registers to it). In the normal
611+
# path the selection result drives the choice exactly as before.
612+
if [ "$_external_t1_adopted" = "true" ]; then
613+
# The external T1 is the fixed anchor; FLAIR registers TO it.
614+
export PIPELINE_REFERENCE_MODALITY="T1"
615+
export PIPELINE_REFERENCE_FILE="${t1_file}"
616+
export PIPELINE_MOVING_FILE="${flair_file:-}"
617+
elif [ "$selected_modality" = "T1" ]; then
520618
export PIPELINE_REFERENCE_MODALITY="T1"
521619
export PIPELINE_REFERENCE_FILE="$t1_file"
522-
export PIPELINE_MOVING_FILE="$flair_file"
620+
export PIPELINE_MOVING_FILE="${flair_file:-}"
523621
else
524622
export PIPELINE_REFERENCE_MODALITY="FLAIR"
525-
export PIPELINE_REFERENCE_FILE="$flair_file"
526-
export PIPELINE_MOVING_FILE="$t1_file"
623+
export PIPELINE_REFERENCE_FILE="${flair_file:-}"
624+
export PIPELINE_MOVING_FILE="${t1_file:-}"
527625
fi
528-
626+
529627
log_formatted "INFO" "===== Authoritative Reference Space Set ====="
530-
log_message "PIPELINE_REFERENCE_MODALITY: $PIPELINE_REFERENCE_MODALITY"
531-
log_message "PIPELINE_REFERENCE_FILE: $(basename "$PIPELINE_REFERENCE_FILE")"
532-
log_message "PIPELINE_MOVING_FILE: $(basename "$PIPELINE_MOVING_FILE")"
628+
log_message "PIPELINE_REFERENCE_MODALITY: ${PIPELINE_REFERENCE_MODALITY:-}"
629+
log_message "PIPELINE_REFERENCE_FILE: $(basename "${PIPELINE_REFERENCE_FILE:-<unset>}")"
630+
log_message "PIPELINE_MOVING_FILE: $(basename "${PIPELINE_MOVING_FILE:-<unset>}")"
533631
log_message "==========================================="
534632

535633
# Log detailed resolution information about selected scans
536-
if [ -n "$t1_file" ] && [ -n "$flair_file" ]; then
634+
if [ -n "${t1_file:-}" ] && [ -n "${flair_file:-}" ]; then
537635
log_message "======== Selected Scan Information ========"
538636
log_message "T1 scan: $t1_file"
539637
log_message "T1 dimensions: $(fslinfo "$t1_file" | grep -E "^dim[1-3]" | awk '{print $1 "=" $2}' | tr '\n' ' ')"
@@ -546,13 +644,17 @@ run_pipeline() {
546644
log_message "Resolution comparison: $(calculate_pixdim_similarity "$t1_file" "$flair_file")/100"
547645
log_message "======================================="
548646
fi
549-
550-
if [ -z "$t1_file" ]; then
551-
log_error "T1 file not found in $EXTRACT_DIR" $ERR_DATA_MISSING
647+
648+
if [ -z "${t1_file:-}" ]; then
649+
if [ -n "${ANATOMICAL_REFERENCE_T1:-}" ]; then
650+
log_error "No in-study T1 found and ANATOMICAL_REFERENCE_T1 is set but adoption did not succeed" $ERR_DATA_MISSING
651+
else
652+
log_error "No in-study T1 found in ${EXTRACT_DIR} and ANATOMICAL_REFERENCE_T1 is not set" $ERR_DATA_MISSING
653+
fi
552654
return $ERR_DATA_MISSING
553655
fi
554-
555-
if [ -z "$flair_file" ]; then
656+
657+
if [ -z "${flair_file:-}" ]; then
556658
log_error "FLAIR file not found in $EXTRACT_DIR" $ERR_DATA_MISSING
557659
return $ERR_DATA_MISSING
558660
fi

0 commit comments

Comments
 (0)