"""HCP path resolution and preparation."""
from pathlib import Path
from typing import TypedDict
from thesis.core.config import PipelineConfig
from thesis.core.context import ProcessingContext
from thesis.core.logging import get_logger
from thesis.core.utils import resolve_path
from ..common import format_patient_path, resolve_t1_path, resolve_with_fallback
from ..config.values import resolve_hcp_value
logger = get_logger(__name__)
[docs]
class HCPPaths(TypedDict):
"""Resolved filesystem paths for the HCP workflow."""
input_dir: Path
diffusion_dir: Path
bedpostx_dir: Path
samples_base_name: Path
thsamples: list[Path]
phsamples: list[Path]
fsamples: list[Path]
mask_path: Path
t1_path: Path
[docs]
def prepare_hcp_paths(config: PipelineConfig, context: ProcessingContext) -> HCPPaths:
"""Resolve HCP directory paths from config and context."""
diffusion_dirname = format_patient_path(
resolve_hcp_value(config, "diffusion_dir", "T1w/Diffusion"), context.patient_id
)
bedpostx_dirname = format_patient_path(
resolve_hcp_value(config, "bedpostx_dir", "T1w/Diffusion.bedpostX"), context.patient_id
)
input_dir = (
Path(context.input_dir).resolve() if context.input_dir is not None else Path(".").resolve()
)
diffusion_dir = resolve_path(input_dir, diffusion_dirname)
bedpostx_dir = resolve_path(input_dir, bedpostx_dirname)
mask_name = resolve_hcp_value(config, "mask_name", "nodif_brain_mask.nii")
mask_path_cfg = resolve_hcp_value(config, "mask_path")
if mask_path_cfg:
mask_path = resolve_with_fallback(
format_patient_path(mask_path_cfg, context.patient_id),
input_dir,
[context.output_dir, context.data_dir],
)
else:
mask_path = diffusion_dir / mask_name
if not mask_path.exists():
for fb in (input_dir, Path("."), getattr(context, "data_dir", None)):
if fb is None:
continue
fb_path = Path(fb).resolve() / mask_name
if fb_path.exists():
logger.warning(
"Brain mask not found at {}, using fallback: {}",
diffusion_dir / mask_name,
fb_path,
)
mask_path = fb_path
break
# BedpostX sample discovery: scan primary dir then non-recursive/recursive
# fallbacks. Stops at the first dir with matching theta samples; the ph/f
# samples are then read from the same directory.
sample_pattern = "merged_th*samples.nii.gz"
thsamples = sorted(bedpostx_dir.glob(sample_pattern))
found_dir = bedpostx_dir
searched: list[Path] = [bedpostx_dir]
if not thsamples:
for fb in (input_dir, context.output_dir, getattr(context, "data_dir", None), Path(".")):
if fb is None:
continue
fb_path = Path(fb).resolve()
if fb_path == bedpostx_dir.resolve() or fb_path in searched:
continue
searched.append(fb_path)
found = sorted(fb_path.glob(sample_pattern)) or sorted(fb_path.rglob(sample_pattern))
if found:
thsamples = found
found_dir = found[0].parent
logger.warning(
"BedpostX samples not found in {}, using fallback: {}", bedpostx_dir, found_dir
)
break
if not thsamples:
logger.warning(
"No BedpostX theta samples ({}) found under: {}. Run BedpostX or "
"set hcp.bedpostx_dir in your config.",
sample_pattern,
", ".join(str(d) for d in searched),
)
return {
"input_dir": input_dir,
"diffusion_dir": diffusion_dir,
"bedpostx_dir": bedpostx_dir,
"samples_base_name": found_dir / "merged",
"thsamples": thsamples,
"phsamples": sorted(found_dir.glob("merged_ph*samples.nii.gz")),
"fsamples": sorted(found_dir.glob("merged_f*samples.nii.gz")),
"mask_path": mask_path,
"t1_path": resolve_t1_path(config, context),
}
[docs]
def prepare_seed_path(config: PipelineConfig, input_dir: Path) -> Path:
"""Resolve seed ROI path from config.
Defaults to ``<diffusion_dir>/seed_source.nii.gz`` with input_dir/cwd
fallback when ``tractography.seed_roi`` is not configured.
"""
seed_roi = getattr(getattr(config, "tractography", None), "seed_roi", None)
if seed_roi is not None:
patient_id = getattr(config, "patient_id", "") or ""
return resolve_path(input_dir, format_patient_path(str(seed_roi), patient_id))
diffusion_dir = resolve_path(
input_dir, resolve_hcp_value(config, "diffusion_dir", "T1w/Diffusion")
)
seed_path = diffusion_dir / "seed_source.nii.gz"
if seed_path.exists():
return seed_path
for fb in (input_dir, Path(".")):
alt = fb / "seed_source.nii.gz"
if alt.exists():
logger.warning("Seed image not found at {}, using fallback: {}", seed_path, alt)
return alt
return seed_path