Skip to content

paths

tit.paths

BIDS-compliant path management for TI-Toolbox.

Provides a singleton :class:PathManager that resolves all file and directory paths within a BIDS-structured TI-Toolbox project. Paths cover subject anatomical data, SimNIBS derivatives, FastSurfer outputs, analysis results, and optimization runs.

Public API

PathManager Central path resolver — instantiated as a singleton via :func:get_path_manager. get_path_manager Return (and optionally initialise) the global PathManager singleton. reset_path_manager Destroy the singleton so the next call creates a fresh instance.

Examples

from tit.paths import get_path_manager pm = get_path_manager("/data/project") # doctest: +SKIP pm.list_simnibs_subjects() # doctest: +SKIP ['001', '002'] pm.m2m("001") # doctest: +SKIP '/data/project/derivatives/SimNIBS/sub-001/m2m_001'

See Also

tit.constants : Project-wide constants used for directory/file names.

PathManager

PathManager(project_dir: str | None = None)

BIDS-compliant path resolution for TI-Toolbox projects.

Provides methods to resolve file and directory paths within a BIDS-structured project, including subject anatomical data, SimNIBS derivatives, FastSurfer outputs, optimization runs, and analysis results.

The project directory can be set explicitly or auto-detected from the PROJECT_DIR / PROJECT_DIR_NAME environment variables (useful inside Docker containers).

Parameters

project_dir : str or None, optional Root directory of the BIDS project. If None, the directory is auto-detected from environment variables on first access.

Attributes

project_dir : str or None Resolved project root, or None if not yet set / detected. project_dir_name : str or None Basename of :attr:project_dir.

See Also

get_path_manager : Obtain the global singleton instance. reset_path_manager : Destroy the singleton for testing or re-init.

Source code in tit/paths.py
def __init__(self, project_dir: str | None = None):
    self._project_dir: str | None = None
    if project_dir:
        self.project_dir = project_dir

project_dir property writable

project_dir: str | None

Project root directory, auto-detected from environment if unset.

project_dir_name property

project_dir_name: str | None

Basename of :attr:project_dir, or the PROJECT_DIR_NAME env var.

derivatives

derivatives() -> str

Path to <project>/derivatives/.

Source code in tit/paths.py
def derivatives(self) -> str:
    """Path to ``<project>/derivatives/``."""
    return os.path.join(self._root(), "derivatives")

sourcedata

sourcedata() -> str

Path to <project>/sourcedata/.

Source code in tit/paths.py
def sourcedata(self) -> str:
    """Path to ``<project>/sourcedata/``."""
    return os.path.join(self._root(), "sourcedata")

simnibs

simnibs() -> str

Path to <project>/derivatives/SimNIBS/.

Source code in tit/paths.py
def simnibs(self) -> str:
    """Path to ``<project>/derivatives/SimNIBS/``."""
    return os.path.join(self._root(), "derivatives", "SimNIBS")

fastsurfer

fastsurfer() -> str

Path to <project>/derivatives/fastsurfer/.

Source code in tit/paths.py
def fastsurfer(self) -> str:
    """Path to ``<project>/derivatives/fastsurfer/``."""
    return os.path.join(self._root(), "derivatives", "fastsurfer")

freesurfer

freesurfer() -> str

Path to <project>/derivatives/freesurfer/ (legacy, read-only).

TI-Toolbox no longer writes here -- FastSurfer --seg_only replaced recon-all -- but every atlas reader still discovers existing recon-all output so projects processed with older releases keep working.

Source code in tit/paths.py
def freesurfer(self) -> str:
    """Path to ``<project>/derivatives/freesurfer/`` (legacy, read-only).

    TI-Toolbox no longer writes here -- FastSurfer ``--seg_only``
    replaced ``recon-all`` -- but every atlas reader still discovers
    existing ``recon-all`` output so projects processed with older
    releases keep working.
    """
    return os.path.join(self._root(), "derivatives", "freesurfer")

ti_toolbox

ti_toolbox() -> str

Path to <project>/derivatives/ti-toolbox/.

Source code in tit/paths.py
def ti_toolbox(self) -> str:
    """Path to ``<project>/derivatives/ti-toolbox/``."""
    return os.path.join(self._root(), "derivatives", "ti-toolbox")

config_dir

config_dir() -> str

Path to <project>/code/ti-toolbox/config/.

Source code in tit/paths.py
def config_dir(self) -> str:
    """Path to ``<project>/code/ti-toolbox/config/``."""
    return os.path.join(self._root(), "code", "ti-toolbox", "config")

jobs_dir

jobs_dir() -> str

Path to <project>/code/ti-toolbox/jobs/ (job server state).

Source code in tit/paths.py
def jobs_dir(self) -> str:
    """Path to ``<project>/code/ti-toolbox/jobs/`` (job server state)."""
    return os.path.join(self._root(), "code", "ti-toolbox", "jobs")

dot_dir

dot_dir() -> str

Path to <project>/.ti-toolbox/ (not created).

Source code in tit/paths.py
def dot_dir(self) -> str:
    """Path to ``<project>/.ti-toolbox/`` (not created)."""
    return dot_dir(self._root())

cache_root

cache_root() -> str

Path to <project>/.ti-toolbox/cache/ (not created).

Source code in tit/paths.py
def cache_root(self) -> str:
    """Path to ``<project>/.ti-toolbox/cache/`` (not created)."""
    return cache_dir(self._root())

cache

cache(*parts: str) -> str

Path to <project>/.ti-toolbox/cache/<parts...> (not created).

Source code in tit/paths.py
def cache(self, *parts: str) -> str:
    """Path to ``<project>/.ti-toolbox/cache/<parts...>`` (not created)."""
    return cache_dir(self._root(), *parts)

ensure_cache

ensure_cache(*parts: str) -> str

Create and return a cache directory, migrating legacy locations once.

Source code in tit/paths.py
def ensure_cache(self, *parts: str) -> str:
    """Create and return a cache directory, migrating legacy locations once."""
    return ensure_cache_dir(self._root(), *parts)

scene_cache

scene_cache(sid: str) -> str

Path to the scene-payload cache for sid (cache/scene/sub-<id>/).

Source code in tit/paths.py
def scene_cache(self, sid: str) -> str:
    """Path to the scene-payload cache for *sid* (``cache/scene/sub-<id>/``)."""
    return self.cache("scene", f"sub-{validate_subject_id(sid)}")

mask_cache

mask_cache(sid: str) -> str

Path to the prepared-ROI-mask cache for sid (cache/masks/sub-<id>/).

Source code in tit/paths.py
def mask_cache(self, sid: str) -> str:
    """Path to the prepared-ROI-mask cache for *sid* (``cache/masks/sub-<id>/``)."""
    return self.cache("masks", f"sub-{validate_subject_id(sid)}")

viewer_stats_cache

viewer_stats_cache() -> str

Path to the viewer's per-volume statistics sidecars (cache/stats/).

Source code in tit/paths.py
def viewer_stats_cache(self) -> str:
    """Path to the viewer's per-volume statistics sidecars (``cache/stats/``)."""
    return self.cache("stats")

storage_cache

storage_cache() -> str

Path to the project disk-usage scan (cache/storage/storage.json).

Source code in tit/paths.py
def storage_cache(self) -> str:
    """Path to the project disk-usage scan (``cache/storage/storage.json``)."""
    return os.path.join(self.cache("storage"), "storage.json")

montage_config

montage_config() -> str

Path to the montage_list.json configuration file.

Source code in tit/paths.py
def montage_config(self) -> str:
    """Path to the ``montage_list.json`` configuration file."""
    return os.path.join(self.config_dir(), "montage_list.json")

project_status

project_status() -> str

Path to the project_status.json file.

Source code in tit/paths.py
def project_status(self) -> str:
    """Path to the ``project_status.json`` file."""
    return os.path.join(self.config_dir(), "project_status.json")

extensions_config

extensions_config() -> str

Path to the extensions.json configuration file.

Uses the user-level config directory so that extension preferences persist across projects and container restarts.

Source code in tit/paths.py
def extensions_config(self) -> str:
    """Path to the ``extensions.json`` configuration file.

    Uses the user-level config directory so that extension
    preferences persist across projects and container restarts.
    """
    return os.path.join(self.user_config_dir(), "extensions.json")

user_config_dir staticmethod

user_config_dir() -> str

Path to the user-level config directory.

Returns the directory that persists across projects and container restarts. Inside Docker this is /root/.config/ti-toolbox (mounted from the host by the Electron launcher). Outside Docker the platform-native config directory is used:

  • macOS: ~/.config/ti-toolbox
  • Linux: $XDG_CONFIG_HOME/ti-toolbox (default ~/.config)
  • Windows: %APPDATA%/ti-toolbox

The directory is created if it does not exist.

Returns

str Absolute path to the user config directory.

Source code in tit/paths.py
@staticmethod
def user_config_dir() -> str:
    """Path to the user-level config directory.

    Returns the directory that persists across projects and container
    restarts.  Inside Docker this is ``/root/.config/ti-toolbox``
    (mounted from the host by the Electron launcher).  Outside Docker
    the platform-native config directory is used:

    - **macOS**: ``~/.config/ti-toolbox``
    - **Linux**: ``$XDG_CONFIG_HOME/ti-toolbox`` (default ``~/.config``)
    - **Windows**: ``%APPDATA%/ti-toolbox``

    The directory is created if it does not exist.

    Returns
    -------
    str
        Absolute path to the user config directory.
    """
    import platform as _platform

    def _usable_dir(path: str | None) -> str | None:
        if not path:
            return None
        try:
            os.makedirs(path, exist_ok=True)
        except OSError:
            return None
        return path if os.path.isdir(path) else None

    # Inside Docker the Electron launcher mounts the host config here.
    # Prefer the in-container mount when present; TIT_USER_CONFIG is a
    # host-side path used by docker-compose and may not exist in-container.
    docker_path = _usable_dir(const.USER_CONFIG_CONTAINER_PATH)
    if docker_path:
        return docker_path

    # Explicit override for non-standard/container-less launches.
    env_path = _usable_dir(os.environ.get("TIT_USER_CONFIG"))
    if env_path:
        return env_path

    # Outside Docker: platform-native paths
    system = _platform.system()
    if system == "Darwin":
        # Use ~/.config (NOT ~/Library/Application Support which is
        # Electron's userData dir).  Matches env.js getUserConfigDir().
        base = os.path.join(os.path.expanduser("~"), ".config")
    elif system == "Windows":
        base = os.environ.get(
            "APPDATA", os.path.join(os.path.expanduser("~"), "AppData", "Roaming")
        )
    else:  # Linux / other
        base = os.environ.get(
            "XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")
        )

    config_dir = os.path.join(base, "ti-toolbox")
    os.makedirs(config_dir, exist_ok=True)
    return config_dir

reports

reports() -> str

Path to <project>/derivatives/ti-toolbox/reports/.

Source code in tit/paths.py
def reports(self) -> str:
    """Path to ``<project>/derivatives/ti-toolbox/reports/``."""
    return os.path.join(self.ti_toolbox(), "reports")

stats_data

stats_data() -> str

Path to <project>/derivatives/ti-toolbox/stats/data/.

Source code in tit/paths.py
def stats_data(self) -> str:
    """Path to ``<project>/derivatives/ti-toolbox/stats/data/``."""
    return os.path.join(self.ti_toolbox(), "stats", "data")

stats_output

stats_output(analysis_type: str, analysis_name: str) -> str

Path to a specific statistics output directory.

Parameters

analysis_type : str Type of statistical analysis (e.g., "permutation"). analysis_name : str Name of the analysis run.

Returns

str Absolute path to the output directory.

Source code in tit/paths.py
def stats_output(self, analysis_type: str, analysis_name: str) -> str:
    """Path to a specific statistics output directory.

    Parameters
    ----------
    analysis_type : str
        Type of statistical analysis (e.g., ``"permutation"``).
    analysis_name : str
        Name of the analysis run.

    Returns
    -------
    str
        Absolute path to the output directory.
    """
    return self._under(
        self.ti_toolbox(),
        "stats",
        validate_name(analysis_type, "analysis type"),
        validate_name(analysis_name, "analysis name"),
    )

logs_group

logs_group() -> str

Path to group-analysis log directory.

Source code in tit/paths.py
def logs_group(self) -> str:
    """Path to group-analysis log directory."""
    return os.path.join(self.ti_toolbox(), "logs", "group_analysis")

qsiprep

qsiprep() -> str

Path to <project>/derivatives/qsiprep/.

Source code in tit/paths.py
def qsiprep(self) -> str:
    """Path to ``<project>/derivatives/qsiprep/``."""
    return os.path.join(self._root(), "derivatives", "qsiprep")

qsirecon

qsirecon() -> str

Path to <project>/derivatives/qsirecon/.

Source code in tit/paths.py
def qsirecon(self) -> str:
    """Path to ``<project>/derivatives/qsirecon/``."""
    return os.path.join(self._root(), "derivatives", "qsirecon")

sub

sub(sid: str) -> str

Path to derivatives/SimNIBS/sub-{sid}/.

Parameters

sid : str Subject identifier (without sub- prefix).

Returns

str Absolute path to the subject's SimNIBS directory.

Source code in tit/paths.py
def sub(self, sid: str) -> str:
    """Path to ``derivatives/SimNIBS/sub-{sid}/``.

    Parameters
    ----------
    sid : str
        Subject identifier (without ``sub-`` prefix).

    Returns
    -------
    str
        Absolute path to the subject's SimNIBS directory.
    """
    return self._under(self.simnibs(), f"sub-{validate_subject_id(sid)}")

m2m

m2m(sid: str) -> str

Path to the m2m_{sid} head-model directory for sid.

Source code in tit/paths.py
def m2m(self, sid: str) -> str:
    """Path to the ``m2m_{sid}`` head-model directory for *sid*."""
    return self._under(self.sub(sid), f"m2m_{sid}")

eeg_positions

eeg_positions(sid: str) -> str

Path to the EEG electrode-position directory for sid.

Source code in tit/paths.py
def eeg_positions(self, sid: str) -> str:
    """Path to the EEG electrode-position directory for *sid*."""
    return os.path.join(self.m2m(sid), "eeg_positions")

rois

rois(sid: str) -> str

Path to the ROI directory for sid.

Source code in tit/paths.py
def rois(self, sid: str) -> str:
    """Path to the ROI directory for *sid*."""
    return os.path.join(self.m2m(sid), "ROIs")

masks

masks(sid: str) -> str

Path to the user-supplied custom mask directory for sid.

Any integer label volume placed here (*.nii.gz/*.nii/*.mgz, in the subject space m2m_{sid} was built from) is auto-discovered as a targetable volume atlas by the optimiser ROI pickers.

Source code in tit/paths.py
def masks(self, sid: str) -> str:
    """Path to the user-supplied custom mask directory for *sid*.

    Any integer label volume placed here (``*.nii.gz``/``*.nii``/``*.mgz``,
    in the subject space ``m2m_{sid}`` was built from) is auto-discovered as
    a targetable volume atlas by the optimiser ROI pickers.
    """
    return os.path.join(self.m2m(sid), "masks")

t1

t1(sid: str) -> str

Path to the T1-weighted NIfTI image for sid.

Source code in tit/paths.py
def t1(self, sid: str) -> str:
    """Path to the T1-weighted NIfTI image for *sid*."""
    return os.path.join(self.m2m(sid), "T1.nii.gz")

segmentation

segmentation(sid: str) -> str

Path to the segmentation directory for sid.

Source code in tit/paths.py
def segmentation(self, sid: str) -> str:
    """Path to the segmentation directory for *sid*."""
    return os.path.join(self.m2m(sid), "segmentation")

tissue_labeling

tissue_labeling(sid: str) -> str

Path to the tissue labeling NIfTI for sid.

Source code in tit/paths.py
def tissue_labeling(self, sid: str) -> str:
    """Path to the tissue labeling NIfTI for *sid*."""
    return os.path.join(self.segmentation(sid), "labeling.nii.gz")

leadfields

leadfields(sid: str) -> str

Path to the leadfields directory for sid.

Source code in tit/paths.py
def leadfields(self, sid: str) -> str:
    """Path to the leadfields directory for *sid*."""
    return os.path.join(self.sub(sid), "leadfields")

forward

forward(sid: str) -> str

Path to the EEG source-forward directory for sid.

Holds the MNE forward solution, source space, head<->MRI transform, fsaverage morph, and the point-electrode leadfield they derive from (derivatives/SimNIBS/sub-{sid}/forward/). Distinct from :meth:leadfields, which the optimizer uses for modeled-electrode stimulation leadfields.

Source code in tit/paths.py
def forward(self, sid: str) -> str:
    """Path to the EEG source-forward directory for *sid*.

    Holds the MNE forward solution, source space, head<->MRI transform,
    fsaverage morph, and the point-electrode leadfield they derive from
    (``derivatives/SimNIBS/sub-{sid}/forward/``).  Distinct from
    :meth:`leadfields`, which the optimizer uses for modeled-electrode
    stimulation leadfields.
    """
    return os.path.join(self.sub(sid), "forward")

simulations

simulations(sid: str) -> str

Path to Simulations/ for sid.

Source code in tit/paths.py
def simulations(self, sid: str) -> str:
    """Path to ``Simulations/`` for *sid*."""
    return os.path.join(self.sub(sid), "Simulations")

logs

logs(sid: str) -> str

Path to per-subject log directory for sid.

Source code in tit/paths.py
def logs(self, sid: str) -> str:
    """Path to per-subject log directory for *sid*."""
    return self._under(self.ti_toolbox(), "logs", f"sub-{validate_subject_id(sid)}")

tissue_analysis_output

tissue_analysis_output(sid: str) -> str

Path to tissue-analysis output directory for sid.

Source code in tit/paths.py
def tissue_analysis_output(self, sid: str) -> str:
    """Path to tissue-analysis output directory for *sid*."""
    return self._under(
        self.ti_toolbox(), "tissue_analysis", f"sub-{validate_subject_id(sid)}"
    )

bids_subject

bids_subject(sid: str) -> str

Path to <project>/sub-{sid}/ (raw BIDS subject root).

Source code in tit/paths.py
def bids_subject(self, sid: str) -> str:
    """Path to ``<project>/sub-{sid}/`` (raw BIDS subject root)."""
    return self._under(self._root(), f"sub-{validate_subject_id(sid)}")

bids_datatype

bids_datatype(sid: str, datatype: str) -> str

Path to <project>/sub-{sid}/{datatype}/ for any BIDS datatype.

Source code in tit/paths.py
def bids_datatype(self, sid: str, datatype: str) -> str:
    """Path to ``<project>/sub-{sid}/{datatype}/`` for any BIDS datatype."""
    return self._under(self.bids_subject(sid), validate_name(datatype, "datatype"))

bids_anat

bids_anat(sid: str) -> str

Path to <project>/sub-{sid}/anat/.

Source code in tit/paths.py
def bids_anat(self, sid: str) -> str:
    """Path to ``<project>/sub-{sid}/anat/``."""
    return self.bids_datatype(sid, "anat")

bids_dwi

bids_dwi(sid: str) -> str

Path to <project>/sub-{sid}/dwi/.

Source code in tit/paths.py
def bids_dwi(self, sid: str) -> str:
    """Path to ``<project>/sub-{sid}/dwi/``."""
    return self.bids_datatype(sid, "dwi")

sourcedata_subject

sourcedata_subject(sid: str) -> str

Path to sourcedata/sub-{sid}/.

Source code in tit/paths.py
def sourcedata_subject(self, sid: str) -> str:
    """Path to ``sourcedata/sub-{sid}/``."""
    return self._under(self.sourcedata(), f"sub-{validate_subject_id(sid)}")

fastsurfer_subject

fastsurfer_subject(sid: str) -> str

Path to derivatives/fastsurfer/sub-{sid}/.

Source code in tit/paths.py
def fastsurfer_subject(self, sid: str) -> str:
    """Path to ``derivatives/fastsurfer/sub-{sid}/``."""
    return self._under(self.fastsurfer(), f"sub-{validate_subject_id(sid)}")

fastsurfer_mri

fastsurfer_mri(sid: str) -> str

Path to derivatives/fastsurfer/sub-{sid}/mri/.

Holds aparc.DKTatlas+aseg.deep.mgz (plus the .nii.gz copy and the *_labels.txt sidecar :mod:tit.pre.fastsurfer writes).

Source code in tit/paths.py
def fastsurfer_mri(self, sid: str) -> str:
    """Path to ``derivatives/fastsurfer/sub-{sid}/mri/``.

    Holds ``aparc.DKTatlas+aseg.deep.mgz`` (plus the ``.nii.gz`` copy and
    the ``*_labels.txt`` sidecar :mod:`tit.pre.fastsurfer` writes).
    """
    return os.path.join(self.fastsurfer_subject(sid), "mri")

freesurfer_subject

freesurfer_subject(sid: str) -> str

Path to derivatives/freesurfer/sub-{sid}/ (legacy, read-only).

Source code in tit/paths.py
def freesurfer_subject(self, sid: str) -> str:
    """Path to ``derivatives/freesurfer/sub-{sid}/`` (legacy, read-only)."""
    return self._under(self.freesurfer(), f"sub-{validate_subject_id(sid)}")

freesurfer_mri

freesurfer_mri(sid: str) -> str

Path to derivatives/freesurfer/sub-{sid}/mri/ (legacy, read-only).

Source code in tit/paths.py
def freesurfer_mri(self, sid: str) -> str:
    """Path to ``derivatives/freesurfer/sub-{sid}/mri/`` (legacy, read-only)."""
    return os.path.join(self.freesurfer_subject(sid), "mri")

qsiprep_subject

qsiprep_subject(sid: str) -> str

Path to derivatives/qsiprep/sub-{sid}/.

Source code in tit/paths.py
def qsiprep_subject(self, sid: str) -> str:
    """Path to ``derivatives/qsiprep/sub-{sid}/``."""
    return self._under(self.qsiprep(), f"sub-{validate_subject_id(sid)}")

qsirecon_subject

qsirecon_subject(sid: str) -> str

Path to derivatives/qsirecon/sub-{sid}/.

Source code in tit/paths.py
def qsirecon_subject(self, sid: str) -> str:
    """Path to ``derivatives/qsirecon/sub-{sid}/``."""
    return self._under(self.qsirecon(), f"sub-{validate_subject_id(sid)}")
ex_search(sid: str) -> str

Path to exhaustive-search results for sid.

Source code in tit/paths.py
def ex_search(self, sid: str) -> str:
    """Path to exhaustive-search results for *sid*."""
    return os.path.join(self.sub(sid), "ex-search")
m_ex_search(sid: str) -> str

Path to multipolar exhaustive-search results for sid.

Source code in tit/paths.py
def m_ex_search(self, sid: str) -> str:
    """Path to multipolar exhaustive-search results for *sid*."""
    return os.path.join(self.sub(sid), "m-ex-search")
flex_search(sid: str) -> str

Path to flex-search results for sid.

Source code in tit/paths.py
def flex_search(self, sid: str) -> str:
    """Path to flex-search results for *sid*."""
    return os.path.join(self.sub(sid), "flex-search")

simulation

simulation(sid: str, sim: str) -> str

Path to a named simulation directory for sid.

Source code in tit/paths.py
def simulation(self, sid: str, sim: str) -> str:
    """Path to a named simulation directory for *sid*."""
    return self._under(self.simulations(sid), validate_name(sim, "simulation"))

sim_fsaverage

sim_fsaverage(sid: str, sim: str) -> str

Path to the fsaverage field-map cache for a simulation.

derivatives/SimNIBS/sub-{sid}/Simulations/{sim}/fsaverage/ -- holds the .npz field projection produced by :func:tit.source.project_fields_to_fsaverage. Co-located with the simulation it derives from (and mirroring SimNIBS's native map_to_fsavg layout), a sibling of TI/ and high_Frequency/ -- distinct from :meth:forward, which is EEG source reconstruction.

Source code in tit/paths.py
def sim_fsaverage(self, sid: str, sim: str) -> str:
    """Path to the fsaverage field-map cache for a simulation.

    ``derivatives/SimNIBS/sub-{sid}/Simulations/{sim}/fsaverage/`` -- holds
    the ``.npz`` field projection produced by
    :func:`tit.source.project_fields_to_fsaverage`.  Co-located with the
    simulation it derives from (and mirroring SimNIBS's native
    ``map_to_fsavg`` layout), a sibling of ``TI/`` and ``high_Frequency/`` --
    distinct from :meth:`forward`, which is EEG source reconstruction.
    """
    return os.path.join(self.simulation(sid, sim), "fsaverage")

ti_mesh

ti_mesh(sid: str, sim: str) -> str

Path to the TI mesh file ({sim}_TI.msh).

Source code in tit/paths.py
def ti_mesh(self, sid: str, sim: str) -> str:
    """Path to the TI mesh file (``{sim}_TI.msh``)."""
    return self._under(self.simulation(sid, sim), "TI", "mesh", f"{sim}_TI.msh")

ti_mesh_dir

ti_mesh_dir(sid: str, sim: str) -> str

Path to the TI mesh directory.

Source code in tit/paths.py
def ti_mesh_dir(self, sid: str, sim: str) -> str:
    """Path to the TI mesh directory."""
    return os.path.join(self.simulation(sid, sim), "TI", "mesh")

ti_central_surface

ti_central_surface(sid: str, sim: str) -> str

Path to the TI central cortical surface mesh.

Source code in tit/paths.py
def ti_central_surface(self, sid: str, sim: str) -> str:
    """Path to the TI central cortical surface mesh."""
    return self._under(
        self.simulation(sid, sim), "TI", "mesh", "surfaces", f"{sim}_TI_central.msh"
    )

mti_central_surface

mti_central_surface(sid: str, sim: str) -> str

Path to the mTI central cortical surface mesh.

mTI runs write their own central surface under mTI/mesh/surfaces/; the TI/mesh/surfaces/ copy is written only by 2-pair TI runs.

Source code in tit/paths.py
def mti_central_surface(self, sid: str, sim: str) -> str:
    """Path to the mTI central cortical surface mesh.

    mTI runs write their own central surface under ``mTI/mesh/surfaces/``;
    the ``TI/mesh/surfaces/`` copy is written only by 2-pair TI runs.
    """
    return self._under(
        self.simulation(sid, sim),
        "mTI",
        "mesh",
        "surfaces",
        f"{sim}_mTI_central.msh",
    )

mti_mesh_dir

mti_mesh_dir(sid: str, sim: str) -> str

Path to the mTI mesh directory.

Source code in tit/paths.py
def mti_mesh_dir(self, sid: str, sim: str) -> str:
    """Path to the mTI mesh directory."""
    return os.path.join(self.simulation(sid, sim), "mTI", "mesh")

analysis_dir

analysis_dir(sid: str, sim: str, space: str) -> str

Path to the analysis directory for a given analysis space.

Parameters

sid : str Subject identifier. sim : str Simulation name. space : str Analysis space — "mesh" or "voxel".

Returns

str Absolute path to Analyses/Mesh/ or Analyses/Voxel/.

Source code in tit/paths.py
def analysis_dir(self, sid: str, sim: str, space: str) -> str:
    """Path to the analysis directory for a given analysis space.

    Parameters
    ----------
    sid : str
        Subject identifier.
    sim : str
        Simulation name.
    space : str
        Analysis space — ``"mesh"`` or ``"voxel"``.

    Returns
    -------
    str
        Absolute path to ``Analyses/Mesh/`` or ``Analyses/Voxel/``.
    """
    folder = "Mesh" if space.lower() == "mesh" else "Voxel"
    return os.path.join(self.simulation(sid, sim), "Analyses", folder)

sourcedata_dicom

sourcedata_dicom(sid: str, modality: str) -> str

Path to DICOM source data for sid and modality.

Source code in tit/paths.py
def sourcedata_dicom(self, sid: str, modality: str) -> str:
    """Path to DICOM source data for *sid* and *modality*."""
    return self._under(
        self.sourcedata_subject(sid), validate_name(modality, "modality"), "dicom"
    )

ex_search_run

ex_search_run(sid: str, run: str) -> str

Path to a specific exhaustive-search run directory.

Source code in tit/paths.py
def ex_search_run(self, sid: str, run: str) -> str:
    """Path to a specific exhaustive-search run directory."""
    return self._under(self.ex_search(sid), validate_name(run, "run"))

m_ex_search_run

m_ex_search_run(sid: str, run: str) -> str

Path to a specific multipolar exhaustive-search run directory.

Source code in tit/paths.py
def m_ex_search_run(self, sid: str, run: str) -> str:
    """Path to a specific multipolar exhaustive-search run directory."""
    return self._under(self.m_ex_search(sid), validate_name(run, "run"))

flex_search_run

flex_search_run(sid: str, name: str) -> str

Path to a specific flex-search run directory.

Source code in tit/paths.py
def flex_search_run(self, sid: str, name: str) -> str:
    """Path to a specific flex-search run directory."""
    return self._under(self.flex_search(sid), validate_name(name, "run"))

flex_electrode_positions

flex_electrode_positions(sid: str, name: str) -> str

Path to electrode_positions.json for a flex-search run.

Source code in tit/paths.py
def flex_electrode_positions(self, sid: str, name: str) -> str:
    """Path to ``electrode_positions.json`` for a flex-search run."""
    return os.path.join(self.flex_search_run(sid, name), "electrode_positions.json")

flex_manifest

flex_manifest(sid: str, name: str) -> str

Path to flex_meta.json for a flex-search run.

Source code in tit/paths.py
def flex_manifest(self, sid: str, name: str) -> str:
    """Path to ``flex_meta.json`` for a flex-search run."""
    return os.path.join(self.flex_search_run(sid, name), "flex_meta.json")

ensure

ensure(path: str) -> str

Create a directory (with parents) if it does not exist.

Parameters

path : str Directory path to create.

Returns

str The same path, for convenient chaining.

Source code in tit/paths.py
def ensure(self, path: str) -> str:
    """Create a directory (with parents) if it does not exist.

    Parameters
    ----------
    path : str
        Directory path to create.

    Returns
    -------
    str
        The same *path*, for convenient chaining.
    """
    os.makedirs(path, exist_ok=True)
    return path

list_bids_subjects

list_bids_subjects() -> list[str]

Subject ids with a raw BIDS folder (<project>/sub-*), naturally sorted.

Source code in tit/paths.py
def list_bids_subjects(self) -> list[str]:
    """Subject ids with a raw BIDS folder (``<project>/sub-*``), naturally sorted."""
    if not self.project_dir:
        return []
    return self._list_sub_dirs(self.project_dir)

list_fastsurfer_subjects

list_fastsurfer_subjects() -> list[str]

Subject ids with a derivatives/fastsurfer/sub-* folder, naturally sorted.

Source code in tit/paths.py
def list_fastsurfer_subjects(self) -> list[str]:
    """Subject ids with a ``derivatives/fastsurfer/sub-*`` folder, naturally sorted."""
    if not self.project_dir:
        return []
    return self._list_sub_dirs(self.fastsurfer())

list_freesurfer_subjects

list_freesurfer_subjects() -> list[str]

Subject ids with a derivatives/freesurfer/sub-* folder (legacy).

Kept so projects carrying old recon-all output still list those subjects; nothing in the toolbox writes this tree any more.

Source code in tit/paths.py
def list_freesurfer_subjects(self) -> list[str]:
    """Subject ids with a ``derivatives/freesurfer/sub-*`` folder (legacy).

    Kept so projects carrying old ``recon-all`` output still list those
    subjects; nothing in the toolbox writes this tree any more.
    """
    if not self.project_dir:
        return []
    return self._list_sub_dirs(self.freesurfer())

list_simnibs_subjects

list_simnibs_subjects() -> list[str]

List subject IDs that have a SimNIBS head-model (m2m) folder.

Returns

list of str Naturally sorted subject identifiers (without the sub- prefix). Returns an empty list if the SimNIBS directory does not exist.

Source code in tit/paths.py
def list_simnibs_subjects(self) -> list[str]:
    """List subject IDs that have a SimNIBS head-model (m2m) folder.

    Returns
    -------
    list of str
        Naturally sorted subject identifiers (without the ``sub-`` prefix).
        Returns an empty list if the SimNIBS directory does not exist.
    """
    if not self.project_dir:
        return []
    return [
        sid
        for sid in self._list_sub_dirs(self.simnibs())
        if os.path.isdir(self.m2m(sid))
    ]

list_simulations

list_simulations(sid: str) -> list[str]

List simulation folder names for a subject.

Parameters

sid : str Subject identifier.

Returns

list of str Alphabetically sorted simulation directory names. Returns an empty list if the Simulations/ directory does not exist.

Source code in tit/paths.py
def list_simulations(self, sid: str) -> list[str]:
    """List simulation folder names for a subject.

    Parameters
    ----------
    sid : str
        Subject identifier.

    Returns
    -------
    list of str
        Alphabetically sorted simulation directory names.  Returns an
        empty list if the ``Simulations/`` directory does not exist.
    """
    sim_root = self.simulations(sid)

    try:
        simulations: list[str] = []
        with os.scandir(sim_root) as it:
            for entry in it:
                if entry.is_dir() and not entry.name.startswith("."):
                    simulations.append(entry.name)
        simulations.sort()
        return simulations
    except OSError:
        return []

list_eeg_caps

list_eeg_caps(sid: str) -> list[str]

List EEG cap CSV filenames for a subject.

Parameters

sid : str Subject identifier.

Returns

list of str Sorted CSV filenames found in the eeg_positions/ directory.

Source code in tit/paths.py
def list_eeg_caps(self, sid: str) -> list[str]:
    """List EEG cap CSV filenames for a subject.

    Parameters
    ----------
    sid : str
        Subject identifier.

    Returns
    -------
    list of str
        Sorted CSV filenames found in the ``eeg_positions/`` directory.
    """
    eeg_pos_dir = self.eeg_positions(sid) if self.project_dir else None
    if not eeg_pos_dir or not os.path.isdir(eeg_pos_dir):
        return []

    caps = [
        f
        for f in os.listdir(eeg_pos_dir)
        if f.endswith(const.EXT_CSV) and not f.startswith(".")
    ]
    caps.sort()
    return caps

list_flex_search_runs

list_flex_search_runs(sid: str) -> list[str]

List flex-search run directories containing result metadata.

Only directories that contain flex_meta.json or electrode_positions.json are included.

Parameters

sid : str Subject identifier.

Returns

list of str Sorted run directory names.

Source code in tit/paths.py
def list_flex_search_runs(self, sid: str) -> list[str]:
    """List flex-search run directories containing result metadata.

    Only directories that contain ``flex_meta.json`` or
    ``electrode_positions.json`` are included.

    Parameters
    ----------
    sid : str
        Subject identifier.

    Returns
    -------
    list of str
        Sorted run directory names.
    """
    root = self.flex_search(sid) if self.project_dir else None
    if not root or not os.path.isdir(root):
        return []

    try:
        out: list[str] = []
        with os.scandir(root) as it:
            for entry in it:
                if not entry.is_dir() or entry.name.startswith("."):
                    continue
                if os.path.isfile(
                    os.path.join(entry.path, "flex_meta.json")
                ) or os.path.isfile(
                    os.path.join(entry.path, "electrode_positions.json")
                ):
                    out.append(entry.name)
        out.sort()
        return out
    except OSError:
        return []

spherical_analysis_name staticmethod

spherical_analysis_name(x: float, y: float, z: float, radius: float, coordinate_space: str) -> str

Build a canonical folder name for a spherical ROI analysis.

Parameters

x, y, z : float Centre coordinates of the sphere (mm). radius : float Sphere radius (mm). coordinate_space : str "MNI" or "subject".

Returns

str Folder name, e.g. "sphere_x0.00_y0.00_z0.00_r5.0_MNI".

Source code in tit/paths.py
@staticmethod
def spherical_analysis_name(
    x: float, y: float, z: float, radius: float, coordinate_space: str
) -> str:
    """Build a canonical folder name for a spherical ROI analysis.

    Parameters
    ----------
    x, y, z : float
        Centre coordinates of the sphere (mm).
    radius : float
        Sphere radius (mm).
    coordinate_space : str
        ``"MNI"`` or ``"subject"``.

    Returns
    -------
    str
        Folder name, e.g. ``"sphere_x0.00_y0.00_z0.00_r5.0_MNI"``.
    """
    coord_space_suffix = (
        "_MNI" if str(coordinate_space).upper() == "MNI" else "_subject"
    )
    return f"sphere_x{x:.2f}_y{y:.2f}_z{z:.2f}_r{float(radius)}{coord_space_suffix}"

spherical_union_analysis_name classmethod

spherical_union_analysis_name(spheres, coordinate_space: str) -> str

Build a folder name for a union of several spherical ROIs.

Parameters

spheres : sequence of tuple of float One (x, y, z, r) per sphere. coordinate_space : str "MNI" or "subject".

Returns

str Folder name, e.g. "spheres2_x-40.00_y-20.00_z5.00_r5.0+ x40.00_y-20.00_z5.00_r5.0_subject". Long unions are shortened to the first sphere plus a hash of the rest so the name stays within filesystem limits.

Source code in tit/paths.py
@classmethod
def spherical_union_analysis_name(cls, spheres, coordinate_space: str) -> str:
    """Build a folder name for a union of several spherical ROIs.

    Parameters
    ----------
    spheres : sequence of tuple of float
        One ``(x, y, z, r)`` per sphere.
    coordinate_space : str
        ``"MNI"`` or ``"subject"``.

    Returns
    -------
    str
        Folder name, e.g. ``"spheres2_x-40.00_y-20.00_z5.00_r5.0+
        x40.00_y-20.00_z5.00_r5.0_subject"``.  Long unions are shortened
        to the first sphere plus a hash of the rest so the name stays
        within filesystem limits.
    """
    parts = [
        f"x{float(x):.2f}_y{float(y):.2f}_z{float(z):.2f}_r{float(r)}"
        for x, y, z, r in spheres
    ]
    suffix = "_MNI" if str(coordinate_space).upper() == "MNI" else "_subject"
    body = "+".join(parts)
    name = f"spheres{len(parts)}_{body}{suffix}"
    if len(name) > 150:
        digest = hashlib.sha1(body.encode()).hexdigest()[:8]
        name = f"spheres{len(parts)}_{parts[0]}_and{len(parts) - 1}more_{digest}{suffix}"
    return name

cortical_analysis_name classmethod

cortical_analysis_name(*, whole_head: bool, region: str | None, atlas_name: str | None = None, atlas_path: str | None = None) -> str

Build a canonical folder name for a cortical/atlas analysis.

Parameters

whole_head : bool If True, the analysis covers the whole head (no region filter). region : str or None Atlas region label(s). Multiple regions are +-separated. atlas_name : str or None, optional Human-readable atlas name. atlas_path : str or None, optional Filesystem path to the atlas file (used as fallback for naming).

Returns

str Folder name, e.g. "cortical_precentral_DK40" or "whole_head_DK40".

Raises

ValueError If whole_head is False and region is empty or None.

Source code in tit/paths.py
@classmethod
def cortical_analysis_name(
    cls,
    *,
    whole_head: bool,
    region: str | None,
    atlas_name: str | None = None,
    atlas_path: str | None = None,
) -> str:
    """Build a canonical folder name for a cortical/atlas analysis.

    Parameters
    ----------
    whole_head : bool
        If *True*, the analysis covers the whole head (no region filter).
    region : str or None
        Atlas region label(s).  Multiple regions are ``+``-separated.
    atlas_name : str or None, optional
        Human-readable atlas name.
    atlas_path : str or None, optional
        Filesystem path to the atlas file (used as fallback for naming).

    Returns
    -------
    str
        Folder name, e.g. ``"cortical_precentral_DK40"`` or
        ``"whole_head_DK40"``.

    Raises
    ------
    ValueError
        If *whole_head* is *False* and *region* is empty or *None*.
    """
    atlas_clean = cls._atlas_name_clean(atlas_name or atlas_path or "unknown_atlas")
    if whole_head:
        return f"whole_head_{atlas_clean}"
    region_val = str(region or "").strip()
    if not region_val:
        raise ValueError(
            "region is required for cortical analysis unless whole_head=True"
        )
    if "+" in region_val:
        n = len(region_val.split("+"))
        h = hashlib.md5(region_val.encode()).hexdigest()[:8]
        return f"cortical_{n}regions_{atlas_clean}_{h}"
    return f"cortical_{region_val}_{atlas_clean}"

analysis_output_dir

analysis_output_dir(*, sid: str, sim: str, space: str, analysis_type: str, coordinates=None, radius=None, coordinate_space: str = 'subject', spheres=None, whole_head: bool = False, region: str | None = None, atlas_name: str | None = None, atlas_path: str | None = None) -> str

Return the analysis output directory path (does not create it).

Delegates to :meth:spherical_analysis_name or :meth:cortical_analysis_name depending on analysis_type.

Parameters

sid : str Subject identifier. sim : str Simulation name. space : str Analysis space ("mesh" or "voxel"). analysis_type : str "spherical" or "cortical". coordinates : sequence of float or None, optional (x, y, z) centre for spherical analysis. radius : float or None, optional Sphere radius in mm (required when analysis_type is "spherical"). coordinate_space : str, optional "MNI" or "subject". Default is "subject". spheres : sequence of tuple of float or None, optional One (x, y, z, r) per sphere when several spheres are unioned into a single ROI. Takes precedence over coordinates/radius. whole_head : bool, optional Whether cortical analysis covers the whole head. region : str or None, optional Atlas region label(s) for cortical analysis. atlas_name : str or None, optional Atlas name for cortical analysis. atlas_path : str or None, optional Atlas file path for cortical analysis.

Returns

str Absolute path to the analysis output directory.

Raises

ValueError If required parameters for the chosen analysis_type are missing or invalid.

See Also

spherical_analysis_name : Naming convention for spherical ROIs. cortical_analysis_name : Naming convention for cortical/atlas ROIs.

Source code in tit/paths.py
def analysis_output_dir(
    self,
    *,
    sid: str,
    sim: str,
    space: str,
    analysis_type: str,
    coordinates=None,
    radius=None,
    coordinate_space: str = "subject",
    spheres=None,
    whole_head: bool = False,
    region: str | None = None,
    atlas_name: str | None = None,
    atlas_path: str | None = None,
) -> str:
    """Return the analysis output directory path (does not create it).

    Delegates to :meth:`spherical_analysis_name` or
    :meth:`cortical_analysis_name` depending on *analysis_type*.

    Parameters
    ----------
    sid : str
        Subject identifier.
    sim : str
        Simulation name.
    space : str
        Analysis space (``"mesh"`` or ``"voxel"``).
    analysis_type : str
        ``"spherical"`` or ``"cortical"``.
    coordinates : sequence of float or None, optional
        ``(x, y, z)`` centre for spherical analysis.
    radius : float or None, optional
        Sphere radius in mm (required when *analysis_type* is
        ``"spherical"``).
    coordinate_space : str, optional
        ``"MNI"`` or ``"subject"``.  Default is ``"subject"``.
    spheres : sequence of tuple of float or None, optional
        One ``(x, y, z, r)`` per sphere when several spheres are unioned
        into a single ROI.  Takes precedence over *coordinates*/*radius*.
    whole_head : bool, optional
        Whether cortical analysis covers the whole head.
    region : str or None, optional
        Atlas region label(s) for cortical analysis.
    atlas_name : str or None, optional
        Atlas name for cortical analysis.
    atlas_path : str or None, optional
        Atlas file path for cortical analysis.

    Returns
    -------
    str
        Absolute path to the analysis output directory.

    Raises
    ------
    ValueError
        If required parameters for the chosen *analysis_type* are
        missing or invalid.

    See Also
    --------
    spherical_analysis_name : Naming convention for spherical ROIs.
    cortical_analysis_name : Naming convention for cortical/atlas ROIs.
    """
    base = self.analysis_dir(sid, sim, space)
    at = str(analysis_type).lower()
    if at == "spherical":
        if spheres:
            return os.path.join(
                base, self.spherical_union_analysis_name(spheres, coordinate_space)
            )
        if not coordinates or len(coordinates) != 3 or radius is None:
            raise ValueError(
                "coordinates(3) and radius required for spherical analysis"
            )
        name = self.spherical_analysis_name(
            float(coordinates[0]),
            float(coordinates[1]),
            float(coordinates[2]),
            float(radius),
            coordinate_space,
        )
    else:
        name = self.cortical_analysis_name(
            whole_head=bool(whole_head),
            region=region,
            atlas_name=atlas_name,
            atlas_path=atlas_path,
        )
    return os.path.join(base, name)

natural_key

natural_key(text: str) -> list[int | str]

Sort key: sub-2 before sub-10 (digits compared numerically).

Source code in tit/paths.py
def natural_key(text: str) -> list[int | str]:
    """Sort key: ``sub-2`` before ``sub-10`` (digits compared numerically)."""
    return [int(c) if c.isdigit() else c.lower() for c in re.split("([0-9]+)", text)]

resolve_resources_dir

resolve_resources_dir() -> str

Resolve the resources/ directory tit's resource-bearing modules read from.

Resolution order, first match wins:

  1. TIT_RESOURCES_DIR env var, if set and an existing directory -- the override a native (non-Docker) launcher or a packaged install can set explicitly. This is also the only way to point at resources/ from a wheel install today: a wheel only ships the tit package itself (pyproject.toml's [tool.setuptools.packages.find] include = ["tit*"] excludes the top-level resources/ tree entirely), so a packaged app with no override and no /ti-toolbox present has no resources/ directory to find -- moving these files inside the package and reading them via importlib.resources is the real fix, tracked as an N1 follow-up, not attempted here.
  2. /ti-toolbox/resources -- the Docker image's own layout (the container copies the checkout to /ti-toolbox); kept working unchanged for the existing container path.
  3. <repo_root>/resources -- checkout-relative, repo_root being two levels above this file (tit/paths.py -> tit/ -> repo root). What a native run from a git checkout has on disk today. Returned even when it doesn't exist, so callers get a stable, informative path for error messages instead of None.
Examples

resolve_resources_dir() # doctest: +SKIP '/Users/.../TI-toolbox/resources'

Source code in tit/paths.py
def resolve_resources_dir() -> str:
    """Resolve the ``resources/`` directory tit's resource-bearing modules read from.

    Resolution order, first match wins:

    1. ``TIT_RESOURCES_DIR`` env var, if set and an existing directory -- the override a
       native (non-Docker) launcher or a packaged install can set explicitly. This is also
       the *only* way to point at resources/ from a wheel install today: a wheel only ships
       the ``tit`` package itself (``pyproject.toml``'s
       ``[tool.setuptools.packages.find] include = ["tit*"]`` excludes the top-level
       ``resources/`` tree entirely), so a packaged app with no override and no
       ``/ti-toolbox`` present has no resources/ directory to find -- moving these files
       inside the package and reading them via ``importlib.resources`` is the real fix,
       tracked as an N1 follow-up, not attempted here.
    2. ``/ti-toolbox/resources`` -- the Docker image's own layout (the container copies the
       checkout to ``/ti-toolbox``); kept working unchanged for the existing container path.
    3. ``<repo_root>/resources`` -- checkout-relative, ``repo_root`` being two levels above
       this file (``tit/paths.py`` -> ``tit/`` -> repo root). What a native run from a git
       checkout has on disk today. Returned even when it doesn't exist, so callers get a
       stable, informative path for error messages instead of ``None``.

    Examples
    --------
    >>> resolve_resources_dir()  # doctest: +SKIP
    '/Users/.../TI-toolbox/resources'
    """
    env_dir = os.environ.get("TIT_RESOURCES_DIR")
    if env_dir and os.path.isdir(env_dir):
        return env_dir
    container_dir = "/ti-toolbox/resources"
    if os.path.isdir(container_dir):
        return container_dir
    repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(repo_root, "resources")

resolve_resource_path

resolve_resource_path(*parts: str) -> str

resolve_resources_dir() joined with parts.

Examples

resolve_resource_path("amv", "GSN-256.csv") # doctest: +SKIP '/Users/.../TI-toolbox/resources/amv/GSN-256.csv'

Source code in tit/paths.py
def resolve_resource_path(*parts: str) -> str:
    """``resolve_resources_dir()`` joined with *parts*.

    Examples
    --------
    >>> resolve_resource_path("amv", "GSN-256.csv")  # doctest: +SKIP
    '/Users/.../TI-toolbox/resources/amv/GSN-256.csv'
    """
    return os.path.join(resolve_resources_dir(), *parts)

is_valid_subject_id

is_valid_subject_id(sid: object) -> bool

True if sid may be used as a sub-<id> path component.

Source code in tit/paths.py
def is_valid_subject_id(sid: object) -> bool:
    """``True`` if *sid* may be used as a ``sub-<id>`` path component."""
    return isinstance(sid, str) and bool(SUBJECT_ID_RE.match(sid))

validate_subject_id

validate_subject_id(sid: object) -> str

Return sid unchanged, or raise ValueError naming the rule it broke.

Called by every :class:PathManager accessor that puts a subject id in a path, so a traversal cannot reach the filesystem however it entered the process — an API body, a config file, or a script argument.

Source code in tit/paths.py
def validate_subject_id(sid: object) -> str:
    """Return *sid* unchanged, or raise ``ValueError`` naming the rule it broke.

    Called by every :class:`PathManager` accessor that puts a subject id in a path, so a
    traversal cannot reach the filesystem however it entered the process — an API body, a
    config file, or a script argument.
    """
    if not is_valid_subject_id(sid):
        raise ValueError(
            f"invalid subject id {sid!r}: letters, digits, '_' and '-' only, "
            f"starting with a letter or digit, at most 64 characters"
        )
    return sid  # type: ignore[return-value]

is_within

is_within(root: str, path: str) -> bool

True if path resolves inside root (containment check for created directories).

Source code in tit/paths.py
def is_within(root: str, path: str) -> bool:
    """``True`` if *path* resolves inside *root* (containment check for created directories)."""
    root_real = os.path.realpath(root)
    candidate = os.path.realpath(path)
    return candidate == root_real or candidate.startswith(
        root_real.rstrip(os.sep) + os.sep
    )

is_valid_name

is_valid_name(value: object) -> bool

True if value may be used as a single path component.

Source code in tit/paths.py
def is_valid_name(value: object) -> bool:
    """``True`` if *value* may be used as a single path component."""
    return isinstance(value, str) and bool(NAME_RE.match(value)) and ".." not in value

validate_name

validate_name(value: object, what: str = 'name') -> str

Return value unchanged, or raise ValueError naming the rule it broke.

what names the field in the message ("simulation", "montage", ...). This is the allowlist half of the sanitizer contract (see dev/security/SECURITY_MASTER_DOCUMENT.md); :func:resolve_under is the containment half, applied where the component becomes a path.

Source code in tit/paths.py
def validate_name(value: object, what: str = "name") -> str:
    """Return *value* unchanged, or raise ``ValueError`` naming the rule it broke.

    *what* names the field in the message (``"simulation"``, ``"montage"``, ...).
    This is the allowlist half of the sanitizer contract (see
    ``dev/security/SECURITY_MASTER_DOCUMENT.md``); :func:`resolve_under` is the
    containment half, applied where the component becomes a path.
    """
    if not is_valid_name(value):
        raise ValueError(
            f"invalid {what} {value!r}: letters, digits, '.', '_', '-' and inner "
            f"spaces only, not starting with '.', at most 128 characters"
        )
    return value  # type: ignore[return-value]

resolve_under

resolve_under(root: str, *parts: str) -> str

Join parts under root and return the path, or raise if it escapes root.

The lexical half of the sanitizer contract: the normalised join must start with <root>/, so .., an absolute part and an empty result are all refused. Pure string work, no filesystem access, which is why every :class:PathManager accessor can afford it. Symlinks are not followed here: a subject directory linked from another disk keeps resolving for scripts, and it is :func:resolve_within -- at the I/O boundary -- that decides whether the link may be read.

Raises ValueError, the same class :func:validate_subject_id and :func:validate_name raise, so one except ValueError at a route boundary covers all three.

Examples

resolve_under("/p", "derivatives", "sub-01") '/p/derivatives/sub-01' resolve_under("/p", "../etc/passwd") Traceback (most recent call last): ValueError: path '/etc/passwd' escapes '/p'

Source code in tit/paths.py
def resolve_under(root: str, *parts: str) -> str:
    """Join *parts* under *root* and return the path, or raise if it escapes *root*.

    The lexical half of the sanitizer contract: the normalised join must start
    with ``<root>/``, so ``..``, an absolute part and an empty result are all
    refused. Pure string work, no filesystem access, which is why every
    :class:`PathManager` accessor can afford it. Symlinks are not followed here:
    a subject directory linked from another disk keeps resolving for scripts,
    and it is :func:`resolve_within` -- at the I/O boundary -- that decides
    whether the link may be read.

    Raises ``ValueError``, the same class :func:`validate_subject_id` and
    :func:`validate_name` raise, so one ``except ValueError`` at a route
    boundary covers all three.

    Examples
    --------
    >>> resolve_under("/p", "derivatives", "sub-01")
    '/p/derivatives/sub-01'
    >>> resolve_under("/p", "../etc/passwd")
    Traceback (most recent call last):
    ValueError: path '/etc/passwd' escapes '/p'
    """
    root_norm = os.path.normpath(root)
    candidate = os.path.normpath(os.path.join(root_norm, *parts))
    if not candidate.startswith(root_norm.rstrip(os.sep) + os.sep):
        raise ValueError(f"path {candidate!r} escapes {root_norm!r}")
    return candidate

resolve_within

resolve_within(jail: str, path: str) -> str

path with symlinks resolved, or raise if it then lies outside jail.

The physical half of the sanitizer contract, applied right before a read, listing or write: realpath(path) must start with realpath(jail)/. A symlink planted inside the project therefore cannot lead outside it, while a project-local link (a run directory linked from elsewhere in the same project) still works. The value returned is the resolved path, so what is checked is exactly what is opened. Equal to :func:is_within in what it accepts, but it returns the path and raises ValueError.

Examples

resolve_within("/p", "/p/derivatives/x") # doctest: +SKIP '/p/derivatives/x'

Source code in tit/paths.py
def resolve_within(jail: str, path: str) -> str:
    """*path* with symlinks resolved, or raise if it then lies outside *jail*.

    The physical half of the sanitizer contract, applied right before a read,
    listing or write: ``realpath(path)`` must start with ``realpath(jail)/``.
    A symlink planted inside the project therefore cannot lead outside it,
    while a project-local link (a run directory linked from elsewhere in the
    same project) still works. The value returned is the resolved path, so
    what is checked is exactly what is opened. Equal to :func:`is_within` in
    what it accepts, but it returns the path and raises ``ValueError``.

    Examples
    --------
    >>> resolve_within("/p", "/p/derivatives/x")  # doctest: +SKIP
    '/p/derivatives/x'
    """
    jail_real = os.path.realpath(jail)
    resolved = os.path.realpath(path)
    if not resolved.startswith(jail_real.rstrip(os.sep) + os.sep):
        raise ValueError(f"path {path!r} resolves outside {jail_real!r}")
    return resolved

resolve_leaf_within

resolve_leaf_within(jail: str, path: str) -> str

path with its parent resolved and its leaf kept, or raise if either escapes jail.

For writes and deletes: :func:resolve_within follows the leaf, so replacing or unlinking what it returns would touch the target of a planted link instead of the link itself. This form resolves only the parent, keeps the leaf's own name, and checks both the entry and what the leaf points at, so the caller replaces or unlinks the alias -- never the file behind it.

Source code in tit/paths.py
def resolve_leaf_within(jail: str, path: str) -> str:
    """*path* with its parent resolved and its leaf kept, or raise if either escapes *jail*.

    For writes and deletes: :func:`resolve_within` follows the leaf, so
    replacing or unlinking what it returns would touch the *target* of a
    planted link instead of the link itself. This form resolves only the
    parent, keeps the leaf's own name, and checks both the entry and what the
    leaf points at, so the caller replaces or unlinks the alias -- never the
    file behind it.
    """
    jail_real = os.path.realpath(jail)
    parent = os.path.realpath(os.path.dirname(path))
    entry = os.path.normpath(os.path.join(parent, os.path.basename(path)))
    if not entry.startswith(jail_real.rstrip(os.sep) + os.sep):
        raise ValueError(f"path {path!r} resolves outside {jail_real!r}")
    resolve_within(jail_real, entry)
    return entry

dot_dir

dot_dir(project_dir: str) -> str

<project>/.ti-toolbox (not created).

Source code in tit/paths.py
def dot_dir(project_dir: str) -> str:
    """``<project>/.ti-toolbox`` (not created)."""
    return os.path.join(project_dir, DOT_DIR_NAME)

cache_dir

cache_dir(project_dir: str, *parts: str) -> str

<project>/.ti-toolbox/cache/<parts...> (not created).

Source code in tit/paths.py
def cache_dir(project_dir: str, *parts: str) -> str:
    """``<project>/.ti-toolbox/cache/<parts...>`` (not created)."""
    for part in parts:
        if not part or part in (".", "..") or "/" in part or "\\" in part:
            raise ValueError(f"invalid cache path component: {part!r}")
    return os.path.join(dot_dir(project_dir), "cache", *parts)

scene_cache_dir_for

scene_cache_dir_for(project_dir: str, sid: str) -> str

<project>/.ti-toolbox/cache/scene/sub-<id>/ (not created).

Source code in tit/paths.py
def scene_cache_dir_for(project_dir: str, sid: str) -> str:
    """``<project>/.ti-toolbox/cache/scene/sub-<id>/`` (not created)."""
    return cache_dir(project_dir, "scene", f"sub-{validate_subject_id(sid)}")

mask_cache_dir_for

mask_cache_dir_for(project_dir: str, sid: str) -> str

<project>/.ti-toolbox/cache/masks/sub-<id>/ (not created).

Source code in tit/paths.py
def mask_cache_dir_for(project_dir: str, sid: str) -> str:
    """``<project>/.ti-toolbox/cache/masks/sub-<id>/`` (not created)."""
    return cache_dir(project_dir, "masks", f"sub-{validate_subject_id(sid)}")

ensure_bidsignore

ensure_bidsignore(project_dir: str) -> None

Append :data:DOT_BIDSIGNORE_LINE to the project's .bidsignore once.

Idempotent, and never rewrites a line a user put there by hand. A failure (read-only project, for instance) is swallowed: the only consequence is a bids-validator warning, and refusing to serve a scene over it would be worse.

Source code in tit/paths.py
def ensure_bidsignore(project_dir: str) -> None:
    """Append :data:`DOT_BIDSIGNORE_LINE` to the project's ``.bidsignore`` once.

    Idempotent, and never rewrites a line a user put there by hand. A failure
    (read-only project, for instance) is swallowed: the only consequence is a
    bids-validator warning, and refusing to serve a scene over it would be worse.
    """
    target = os.path.join(project_dir, ".bidsignore")
    try:
        with open(target, encoding="utf-8") as fh:
            existing = fh.read().splitlines()
    except OSError:
        existing = []
    if DOT_BIDSIGNORE_LINE in existing:
        return
    lines = [*existing, DOT_BIDSIGNORE_LINE] if existing else [DOT_BIDSIGNORE_LINE]
    try:
        with open(target, "w", encoding="utf-8") as fh:
            fh.write("\n".join(lines) + "\n")
    except OSError:
        pass

migrate_legacy_caches

migrate_legacy_caches(project_dir: str) -> list[tuple[str, str]]

Move pre-.ti-toolbox cache locations into the dot-directory.

Each move is a plain rename, and only ever when the destination does not exist yet, so a half-migrated project can never overwrite a fresh cache. A rename that fails (cross-device, permissions, another process mid-move) is ignored: the legacy directory is then simply left where it is and the new cache is rebuilt, which costs time but never correctness.

Returns the (old, new) pairs actually moved -- for tests and logging.

Source code in tit/paths.py
def migrate_legacy_caches(project_dir: str) -> list[tuple[str, str]]:
    """Move pre-``.ti-toolbox`` cache locations into the dot-directory.

    Each move is a plain rename, and only ever when the destination does not
    exist yet, so a half-migrated project can never overwrite a fresh cache. A
    rename that fails (cross-device, permissions, another process mid-move) is
    ignored: the legacy directory is then simply left where it is and the new
    cache is rebuilt, which costs time but never correctness.

    Returns the ``(old, new)`` pairs actually moved -- for tests and logging.
    """
    moved: list[tuple[str, str]] = []
    pairs = [
        (os.path.join(project_dir, *old), os.path.join(project_dir, *new))
        for old, new in LEGACY_CACHE_MOVES
    ]
    simnibs_dir = os.path.join(project_dir, "derivatives", "SimNIBS")
    try:
        subjects = sorted(os.listdir(simnibs_dir))
    except OSError:
        subjects = []
    for entry in subjects:
        if not entry.startswith("sub-") or not is_valid_subject_id(entry[4:]):
            continue
        sid = entry[4:]
        pairs.append(
            (
                os.path.join(simnibs_dir, entry, f"m2m_{sid}", "masks", ".prepared"),
                cache_dir(project_dir, "masks", entry),
            )
        )
    for old, new in pairs:
        if not os.path.exists(old) or os.path.exists(new):
            continue
        try:
            os.makedirs(os.path.dirname(new), exist_ok=True)
            os.rename(old, new)
        except OSError:
            continue
        moved.append((old, new))
    return moved

ensure_cache_dir

ensure_cache_dir(project_dir: str, *parts: str) -> str

Create <project>/.ti-toolbox/cache/<parts...> and return it.

The first call for a project also migrates the legacy cache locations, drops the README that says the tree is disposable, and lists the dot-directory in .bidsignore.

Source code in tit/paths.py
def ensure_cache_dir(project_dir: str, *parts: str) -> str:
    """Create ``<project>/.ti-toolbox/cache/<parts...>`` and return it.

    The first call for a project also migrates the legacy cache locations,
    drops the README that says the tree is disposable, and lists the
    dot-directory in ``.bidsignore``.
    """
    target = cache_dir(project_dir, *parts)
    if project_dir not in _MIGRATED:
        _MIGRATED.add(project_dir)
        migrate_legacy_caches(project_dir)
        try:
            os.makedirs(dot_dir(project_dir), exist_ok=True)
            readme = os.path.join(dot_dir(project_dir), "README")
            if not os.path.exists(readme):
                with open(readme, "w", encoding="utf-8") as fh:
                    fh.write(_DOT_README)
        except OSError:
            pass
        ensure_bidsignore(project_dir)
    os.makedirs(target, exist_ok=True)
    return target

get_path_manager

get_path_manager(project_dir: str | None = None) -> PathManager

Return the global :class:PathManager singleton.

Creates a new instance on the first call. If project_dir is provided, the singleton's :attr:~PathManager.project_dir is (re)set.

Parameters

project_dir : str or None, optional Project root directory (the BIDS root holding sub-* and derivatives/). Must exist. When None (default), the existing value is kept; if none was set, it is auto-detected from the PROJECT_DIR environment variable or, inside the container, /mnt/<PROJECT_DIR_NAME> -- which is why scripts and notebooks run in the app need no argument.

Returns

PathManager The shared singleton instance. Every path accessor (pm.m2m(sid), pm.simulations(sid), ...) raises RuntimeError("Project directory not set") if no project root could be resolved.

Raises

ValueError If project_dir is given but is not an existing directory.

Examples

from tit import get_path_manager pm = get_path_manager("/data/project") # doctest: +SKIP pm.list_simnibs_subjects() # doctest: +SKIP ['ernie', '101'] pm.m2m("ernie") # doctest: +SKIP '/data/project/derivatives/SimNIBS/sub-ernie/m2m_ernie' get_path_manager() is pm # the same singleton on later calls # doctest: +SKIP True

See Also

reset_path_manager : Destroy the singleton for testing or re-init.

Source code in tit/paths.py
def get_path_manager(project_dir: str | None = None) -> PathManager:
    """Return the global :class:`PathManager` singleton.

    Creates a new instance on the first call.  If *project_dir* is provided,
    the singleton's :attr:`~PathManager.project_dir` is (re)set.

    Parameters
    ----------
    project_dir : str or None, optional
        Project root directory (the BIDS root holding ``sub-*`` and
        ``derivatives/``).  Must exist.  When ``None`` (default), the
        existing value is kept; if none was set, it is auto-detected from
        the ``PROJECT_DIR`` environment variable or, inside the container,
        ``/mnt/<PROJECT_DIR_NAME>`` -- which is why scripts and notebooks
        run in the app need no argument.

    Returns
    -------
    PathManager
        The shared singleton instance.  Every path accessor
        (``pm.m2m(sid)``, ``pm.simulations(sid)``, ...) raises
        ``RuntimeError("Project directory not set")`` if no project root
        could be resolved.

    Raises
    ------
    ValueError
        If *project_dir* is given but is not an existing directory.

    Examples
    --------
    >>> from tit import get_path_manager
    >>> pm = get_path_manager("/data/project")  # doctest: +SKIP
    >>> pm.list_simnibs_subjects()  # doctest: +SKIP
    ['ernie', '101']
    >>> pm.m2m("ernie")  # doctest: +SKIP
    '/data/project/derivatives/SimNIBS/sub-ernie/m2m_ernie'
    >>> get_path_manager() is pm  # the same singleton on later calls  # doctest: +SKIP
    True

    See Also
    --------
    reset_path_manager : Destroy the singleton for testing or re-init.
    """
    global _path_manager_instance
    if _path_manager_instance is None:
        _path_manager_instance = PathManager()
    if project_dir is not None:
        _path_manager_instance.project_dir = project_dir
    return _path_manager_instance

reset_path_manager

reset_path_manager() -> None

Destroy the singleton so the next call creates a fresh instance.

Primarily used in test fixtures to prevent cross-test contamination.

See Also

get_path_manager : Obtain the singleton instance.

Source code in tit/paths.py
def reset_path_manager() -> None:
    """Destroy the singleton so the next call creates a fresh instance.

    Primarily used in test fixtures to prevent cross-test contamination.

    See Also
    --------
    get_path_manager : Obtain the singleton instance.
    """
    global _path_manager_instance
    _path_manager_instance = None