Skip to content

Core Utilities

TI-Toolbox provides opinionated, BIDS-compliant infrastructure for path resolution, logging, constants, and config serialization. These modules enforce a strict project structure so that all pipeline stages produce consistent, discoverable outputs.

graph LR
    IMPORT(["import tit"]) --> LOG[Logger]
    IMPORT --> PM[PathManager]
    IMPORT --> CONFIG[Config IO]
    PM --> PATHS([BIDS Paths])
    LOG --> FILE([File Logs])
    CONFIG --> JSON([JSON Files])
    JSON --> CLI[CLI Subprocesses]
    style IMPORT fill:#1a3a5c,stroke:#48a,color:#fff
    style LOG fill:#2d5a27,stroke:#4a8,color:#fff
    style PM fill:#2d5a27,stroke:#4a8,color:#fff
    style CONFIG fill:#2d5a27,stroke:#4a8,color:#fff
    style CLI fill:#2d5a27,stroke:#4a8,color:#fff
    style PATHS fill:#1a5c4a,stroke:#4a8,color:#fff
    style FILE fill:#1a5c4a,stroke:#4a8,color:#fff
    style JSON fill:#1a5c4a,stroke:#4a8,color:#fff

Initialization

Just import — logging and path resolution are automatic:

from tit.sim import SimulationConfig, run_simulation

Importing any tit module configures the tit logger hierarchy and attaches a stdout handler at INFO level. PathManager auto-detects the project directory from the PROJECT_DIR or PROJECT_DIR_NAME environment variables inside Docker containers. No explicit initialization calls are needed.

PathManager

PathManager is a singleton that enforces the BIDS directory layout. All modules use it instead of constructing paths manually.

Project-Level Paths

These methods take no arguments and return top-level directories or files:

Method Returns
pm.derivatives() <project>/derivatives
pm.sourcedata() <project>/sourcedata
pm.simnibs() <project>/derivatives/SimNIBS
pm.fastsurfer() <project>/derivatives/fastsurfer
pm.freesurfer() <project>/derivatives/freesurfer
pm.ti_toolbox() <project>/derivatives/ti-toolbox
pm.config_dir() <project>/code/ti-toolbox/config
pm.montage_config() <project>/code/ti-toolbox/config/montage_list.json
pm.project_status() <project>/code/ti-toolbox/config/project_status.json
pm.reports() <project>/derivatives/ti-toolbox/reports
pm.stats_data() <project>/derivatives/ti-toolbox/stats/data
pm.qsiprep() <project>/derivatives/qsiprep
pm.qsirecon() <project>/derivatives/qsirecon

Subject-Level Paths

Methods that accept a subject ID (sid) string without the sub- prefix:

Method Returns
pm.sub("001") <simnibs>/sub-001
pm.m2m("001") <simnibs>/sub-001/m2m_001
pm.t1("001") .../m2m_001/T1.nii.gz
pm.segmentation("001") .../m2m_001/segmentation
pm.tissue_labeling("001") .../segmentation/labeling.nii.gz
pm.eeg_positions("001") .../m2m_001/eeg_positions
pm.rois("001") .../m2m_001/ROIs
pm.simulations("001") <simnibs>/sub-001/Simulations
pm.leadfields("001") <simnibs>/sub-001/leadfields
pm.ex_search("001") <simnibs>/sub-001/ex-search
pm.flex_search("001") <simnibs>/sub-001/flex-search
pm.logs("001") <ti-toolbox>/logs/sub-001
pm.tissue_analysis_output("001") <ti-toolbox>/tissue_analysis/sub-001
pm.bids_subject("001") <project>/sub-001
pm.bids_anat("001") <project>/sub-001/anat
pm.bids_dwi("001") <project>/sub-001/dwi
pm.fastsurfer_subject("001") <fastsurfer>/sub-001
pm.fastsurfer_mri("001") <fastsurfer>/sub-001/mri
pm.freesurfer_subject("001") <freesurfer>/sub-001
pm.freesurfer_mri("001") <freesurfer>/sub-001/mri
pm.sourcedata_subject("001") <sourcedata>/sub-001
pm.qsiprep_subject("001") <qsiprep>/sub-001
pm.qsirecon_subject("001") <qsirecon>/sub-001

Simulation-Level Paths

Methods that accept a subject ID and simulation name:

Method Returns
pm.simulation("001", "motor") .../Simulations/motor
pm.ti_mesh("001", "motor") .../TI/mesh/motor_TI.msh
pm.ti_mesh_dir("001", "motor") .../TI/mesh
pm.ti_central_surface("001", "motor") .../TI/mesh/surfaces/motor_TI_central.msh
pm.mti_mesh_dir("001", "motor") .../mTI/mesh
pm.analysis_dir("001", "motor", "mesh") .../Analyses/Mesh
pm.analysis_dir("001", "motor", "voxel") .../Analyses/Voxel

Additional paths for optimization runs and statistics:

Method Returns
pm.flex_search_run("001", "run_01") .../flex-search/run_01
pm.flex_manifest("001", "run_01") .../flex-search/run_01/flex_meta.json
pm.flex_electrode_positions("001", "run_01") .../flex-search/run_01/electrode_positions.json
pm.ex_search_run("001", "run_01") .../ex-search/run_01
pm.sourcedata_dicom("001", "anat") <sourcedata>/sub-001/anat/dicom
pm.stats_output("group_comparison", "motor_study") <ti-toolbox>/stats/group_comparison/motor_study
pm.logs_group() <ti-toolbox>/logs/group_analysis

Listing Methods

pm.list_simnibs_subjects()       # ["001", "002"] — subjects with m2m folders
pm.list_simulations("001")       # ["motor_cortex", "frontal"]
pm.list_eeg_caps("001")          # ["GSN-HydroCel-185.csv"]
pm.list_flex_search_runs("001")  # ["run_01"] — runs with metadata files

Utility

pm.ensure("/some/path")  # creates directory (with parents) and returns the path

Logging

The tit logger hierarchy has propagate=False, isolating it from the root logger. Importing tit attaches its own stdout handler; pipeline modules additionally attach file handlers. Calling setup_logging() alone does not add handlers.

Function Purpose
setup_logging(level) Configure the tit logger level; adds NO handlers
add_file_handler(log_file, level, logger_name) Attach a FileHandler (append mode); creates parent dirs
add_stream_handler(logger_name, level) Attach a StreamHandler (stdout)
get_file_only_logger(name, log_file, level) Return a logger that writes ONLY to the given file

Typical Patterns

Terminal output (automatic on import — nothing to do):

import tit  # auto-initializes: setup_logging("INFO") + add_stream_handler("tit", "INFO")

File logging (opt-in, used by pipeline modules):

from tit import add_file_handler

fh = add_file_handler("/data/logs/run.log", level="DEBUG")

Isolated file logger (used per-analysis):

from tit.logger import get_file_only_logger

log = get_file_only_logger("roi_analysis", "/data/logs/roi.log")
log.info("Analyzing ROI...")

Log Format

File handlers:

2025-01-15 14:30:00 | INFO | tit.sim.simulator | Simulation started

Stream handlers use minimal format: %(message)s.

Constants

All hardcoded values live in tit.constants. Key categories:

Category Examples
Directory names DIR_DERIVATIVES, DIR_SIMNIBS, DIR_FLEX_SEARCH, DIR_ANALYSIS
File names FILE_MONTAGE_LIST, FILE_T1, FILE_EGI_TEMPLATE
File extensions EXT_NIFTI (.nii.gz), EXT_MESH (.msh), EXT_CSV
BIDS prefixes PREFIX_SUBJECT (sub-), PREFIX_SESSION (ses-)
Field names FIELD_TI_MAX (TI_max), FIELD_MTI_MAX (mTI_max), FIELD_TI_NORMAL (TI_normal)
Tissue tags GM_TISSUE_TAG (2), WM_TISSUE_TAG (1), BRAIN_TISSUE_TAG_RANGES
Conductivities CONDUCTIVITY_GRAY_MATTER (0.275 S/m), CONDUCTIVITY_WHITE_MATTER (0.126 S/m), 12 tissues total
Tissue properties TISSUE_PROPERTIES — list of dicts with number, name, conductivity, and reference
Atlas names ATLAS_DK40, ATLAS_A2009S, ATLAS_ASEG, ATLAS_APARC_ASEG
Analysis defaults DEFAULT_PERCENTILES ([95, 99, 99.9]), DEFAULT_FOCALITY_CUTOFFS ([50, 75, 90, 95]), DEFAULT_RADIUS_MM (5.0)
Simulation SIM_TYPE_TI, SIM_TYPE_MTI, ELECTRODE_SHAPE_ELLIPSE, DEFAULT_INTENSITY (1.0)
EEG nets EEG_NETS — list of dicts with value, label, electrode_count
Validation bounds VALIDATION_BOUNDS — min/max for radius, current, iterations, etc.
Plot settings PLOT_DPI (600), PLOT_FIGSIZE_DEFAULT ((10, 8))
Timestamps TIMESTAMP_FORMAT_DEFAULT (%Y%m%d_%H%M%S), TIMESTAMP_FORMAT_READABLE
QSI integration QSI_RECON_SPECS, QSI_ATLASES, QSI_DEFAULT_CPUS (8)
from tit import constants as const

const.FIELD_TI_MAX         # "TI_max"
const.GM_TISSUE_TAG        # 2
const.DEFAULT_RADIUS_MM    # 5.0
const.TISSUE_PROPERTIES    # [{"number": 1, "name": "White Matter", ...}, ...]

Config IO

The tit.config_io module serializes typed config dataclasses to JSON for CLI subprocesses. The job server uses this mechanism to pass configurations to pipeline subprocesses.

from tit.config_io import write_config_json, read_config_json

# Write: dataclass -> temp JSON file, returns path
path = write_config_json(my_flex_config, prefix="flex")

# Read: JSON file -> plain dict
data = read_config_json(path)

Union-typed fields (ROI specs, electrode specs) get a _type discriminator so the subprocess can reconstruct the correct type:

Class _type value
FlexConfig.SphericalROI "SphericalROI"
FlexConfig.AtlasROI "AtlasROI"
FlexConfig.SubcorticalROI "SubcorticalROI"
ExConfig.PoolElectrodes "PoolElectrodes"
ExConfig.BucketElectrodes "BucketElectrodes"
Montage "Montage"

Error Handling

Custom exceptions are defined in domain-specific modules:

Exception Module Base Class When Raised
PreprocessError tit.pre.utils RuntimeError A preprocessing step fails
PreprocessCancelled tit.pre.utils RuntimeError User cancels a preprocessing run
DockerBuildError tit.pre.qsi.docker_builder Exception Docker command construction fails
from tit.pre import run_pipeline
from tit.pre.utils import PreprocessError, PreprocessCancelled

try:
    run_pipeline(subject_ids=["001"], create_m2m=True)
except PreprocessCancelled:
    print("Pipeline was cancelled")
except PreprocessError as e:
    print(f"Pipeline failed: {e}")

API Reference

Path Management

tit.paths.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)

tit.paths.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

tit.paths.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

Logging

tit.logger.setup_logging

setup_logging(level: str = 'INFO') -> None

Configure the tit logger hierarchy.

Sets the log level but adds no handlers of its own — file handlers are attached later via :func:add_file_handler and GUI handlers via Qt signal bridges. The one exception: when $TIT_EVENTS_FILE is set, a :class:JsonEventHandler is attached to both the tit and simnibs loggers (flex/ex/mex/sim already attach their own file handlers to simnibs -- that is where FEM solver progress goes) so every log record also becomes a type: "log" line in the job's events.jsonl, with no per-runner changes needed for plain log output. Structured events (stage/progress/artifact/result/exit) are emitted separately via tit.jobs.events, through the same sink.

Parameters

level : str, optional Logging level name (e.g., "DEBUG", "INFO"). Default is "INFO".

See Also

add_file_handler : Attach a file handler to a named logger. add_stream_handler : Attach a console handler to a named logger. get_event_sink : The sink a JsonEventHandler writes through.

Source code in tit/logger.py
def setup_logging(level: str = "INFO") -> None:
    """Configure the ``tit`` logger hierarchy.

    Sets the log level but adds **no** handlers of its own — file handlers
    are attached later via :func:`add_file_handler` and GUI handlers via Qt
    signal bridges. The one exception: when ``$TIT_EVENTS_FILE`` is set, a
    :class:`JsonEventHandler` is attached to both the ``tit`` and
    ``simnibs`` loggers (flex/ex/mex/sim already attach their own file
    handlers to ``simnibs`` -- that is where FEM solver progress goes) so
    every log record also becomes a ``type: "log"`` line in the job's
    ``events.jsonl``, with no per-runner changes needed for plain log
    output. Structured events (stage/progress/artifact/result/exit) are
    emitted separately via ``tit.jobs.events``, through the same sink.

    Parameters
    ----------
    level : str, optional
        Logging level name (e.g., ``"DEBUG"``, ``"INFO"``).  Default is
        ``"INFO"``.

    See Also
    --------
    add_file_handler : Attach a file handler to a named logger.
    add_stream_handler : Attach a console handler to a named logger.
    get_event_sink : The sink a JsonEventHandler writes through.
    """
    logger = logging.getLogger("tit")
    logger.handlers.clear()
    logger.setLevel(getattr(logging, level.upper(), logging.INFO))
    logger.propagate = False  # never bubble to root/terminal

    # Quiet noisy third-party loggers
    for name in ("matplotlib", "matplotlib.font_manager", "PIL"):
        logging.getLogger(name).setLevel(logging.ERROR)

    sink = get_event_sink()
    if sink is not None:
        for logger_name in ("tit", "simnibs"):
            target = logging.getLogger(logger_name)
            if not any(isinstance(h, JsonEventHandler) for h in target.handlers):
                handler = JsonEventHandler(sink)
                handler.setLevel(logging.DEBUG)
                target.addHandler(handler)
    else:
        logging.getLogger("tit.logger").debug(
            f"${TIT_EVENTS_FILE_ENV} not set; no JsonEventHandler attached"
        )

tit.logger.add_file_handler

add_file_handler(log_file: str | Path, level: str = 'DEBUG', logger_name: str = 'tit') -> FileHandler

Attach a file handler to a named logger.

Creates the parent directory if it does not exist. Returns the handler so callers can remove it when the run completes.

Parameters

log_file : str or pathlib.Path Path to the log file (opened in append mode). level : str, optional Minimum log level for this handler. Default is "DEBUG" so the file captures everything. logger_name : str, optional Logger to attach to. Default is "tit" (the package root).

Returns

logging.FileHandler The handler for log_file on logger_name -- newly created, or the existing one if this exact (logger, path) pair was already attached (a long-lived process, e.g. one ROI loop calling this per iteration, would otherwise leak one handler -- and one open file descriptor -- per call).

See Also

setup_logging : Set the package-wide log level. add_stream_handler : Attach a console (stdout) handler. get_file_only_logger : Create an isolated file-only logger.

Source code in tit/logger.py
def add_file_handler(
    log_file: str | Path,
    level: str = "DEBUG",
    logger_name: str = "tit",
) -> logging.FileHandler:
    """Attach a file handler to a named logger.

    Creates the parent directory if it does not exist.  Returns the handler
    so callers can remove it when the run completes.

    Parameters
    ----------
    log_file : str or pathlib.Path
        Path to the log file (opened in append mode).
    level : str, optional
        Minimum log level for this handler.  Default is ``"DEBUG"`` so the
        file captures everything.
    logger_name : str, optional
        Logger to attach to.  Default is ``"tit"`` (the package root).

    Returns
    -------
    logging.FileHandler
        The handler for *log_file* on *logger_name* -- newly created, or the
        existing one if this exact (logger, path) pair was already attached
        (a long-lived process, e.g. one ROI loop calling this per iteration,
        would otherwise leak one handler -- and one open file descriptor --
        per call).

    See Also
    --------
    setup_logging : Set the package-wide log level.
    add_stream_handler : Attach a console (stdout) handler.
    get_file_only_logger : Create an isolated file-only logger.
    """
    log_file = Path(log_file)
    log_file.parent.mkdir(parents=True, exist_ok=True)
    logger = logging.getLogger(logger_name)
    resolved = str(log_file.resolve())
    for existing in logger.handlers:
        if (
            isinstance(existing, logging.FileHandler)
            and getattr(existing, "baseFilename", None) == resolved
        ):
            return existing
    fh = logging.FileHandler(str(log_file), mode="a")
    fh.setLevel(getattr(logging, level.upper(), logging.DEBUG))
    fh.setFormatter(logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT))
    logger.addHandler(fh)
    return fh

tit.logger.add_stream_handler

add_stream_handler(logger_name: str = 'tit', level: str = 'INFO') -> StreamHandler

Attach a stdout handler to a named logger.

Used by scripts for terminal output and by __main__ entry points so that BaseProcessThread can capture subprocess stdout for the GUI.

Parameters

logger_name : str, optional Logger to attach to. Default is "tit". level : str, optional Minimum log level. Default is "INFO".

Returns

logging.StreamHandler The newly created handler.

See Also

setup_logging : Set the package-wide log level. add_file_handler : Attach a file handler.

Source code in tit/logger.py
def add_stream_handler(
    logger_name: str = "tit",
    level: str = "INFO",
) -> logging.StreamHandler:
    """Attach a stdout handler to a named logger.

    Used by scripts for terminal output and by ``__main__`` entry points
    so that ``BaseProcessThread`` can capture subprocess stdout for the GUI.

    Parameters
    ----------
    logger_name : str, optional
        Logger to attach to.  Default is ``"tit"``.
    level : str, optional
        Minimum log level.  Default is ``"INFO"``.

    Returns
    -------
    logging.StreamHandler
        The newly created handler.

    See Also
    --------
    setup_logging : Set the package-wide log level.
    add_file_handler : Attach a file handler.
    """
    import sys

    handler = logging.StreamHandler(sys.stdout)
    handler.setLevel(getattr(logging, level.upper(), logging.INFO))
    handler.setFormatter(logging.Formatter("%(message)s"))
    logger = logging.getLogger(logger_name)
    logger.addHandler(handler)
    return handler

tit.logger.get_file_only_logger

get_file_only_logger(name: str, log_file: str | Path, level: str = 'DEBUG') -> Logger

Return a logger that writes only to log_file — no console output.

If a logger with name already exists its handlers are replaced so that repeated calls (e.g., across ROIs) always point at the correct file.

Parameters

name : str Logger name (should be unique per use-case). log_file : str or pathlib.Path Path to the log file. level : str, optional Minimum log level. Default is "DEBUG".

Returns

logging.Logger A configured logger with a single file handler and propagate=False.

See Also

add_file_handler : Lower-level helper used internally.

Source code in tit/logger.py
def get_file_only_logger(
    name: str,
    log_file: str | Path,
    level: str = "DEBUG",
) -> logging.Logger:
    """Return a logger that writes **only** to *log_file* — no console output.

    If a logger with *name* already exists its handlers are replaced so that
    repeated calls (e.g., across ROIs) always point at the correct file.

    Parameters
    ----------
    name : str
        Logger name (should be unique per use-case).
    log_file : str or pathlib.Path
        Path to the log file.
    level : str, optional
        Minimum log level.  Default is ``"DEBUG"``.

    Returns
    -------
    logging.Logger
        A configured logger with a single file handler and
        ``propagate=False``.

    See Also
    --------
    add_file_handler : Lower-level helper used internally.
    """
    logger = logging.getLogger(name)
    logger.handlers.clear()
    logger.setLevel(getattr(logging, level.upper(), logging.DEBUG))
    logger.propagate = False  # never bubble to root/terminal
    add_file_handler(log_file, level=level, logger_name=name)
    return logger

Config IO

tit.config_io.serialize_config

serialize_config(config: Any) -> dict[str, Any]

Convert a config dataclass to a JSON-serialisable dict.

Handles Enum fields (via .value), nested dataclasses (recursed), union-typed ROI / electrode specs (injects a _type discriminator), and None values (preserved as JSON null).

Also injects project_dir from the active :class:~tit.paths.PathManager so that subprocess entry points can initialise their own singleton.

Parameters

config : dataclass instance Any config dataclass (e.g., FlexConfig, ExConfig).

Returns

dict JSON-serialisable dictionary representation of config.

See Also

write_config_json : Serialise and write to a temp file in one step. read_config_json : Read a JSON config back into a dict.

Source code in tit/config_io.py
def serialize_config(config: Any) -> dict[str, Any]:
    """Convert a config dataclass to a JSON-serialisable dict.

    Handles Enum fields (via ``.value``), nested dataclasses (recursed),
    union-typed ROI / electrode specs (injects a ``_type`` discriminator),
    and *None* values (preserved as JSON ``null``).

    Also injects ``project_dir`` from the active :class:`~tit.paths.PathManager`
    so that subprocess entry points can initialise their own singleton.

    Parameters
    ----------
    config : dataclass instance
        Any config dataclass (e.g., ``FlexConfig``, ``ExConfig``).

    Returns
    -------
    dict
        JSON-serialisable dictionary representation of *config*.

    See Also
    --------
    write_config_json : Serialise and write to a temp file in one step.
    read_config_json : Read a JSON config back into a dict.
    """
    data = _serialize(config)
    # Inject project_dir for subprocess entry points
    from tit.paths import get_path_manager

    data["project_dir"] = get_path_manager().project_dir
    return data

tit.config_io.write_config_json

write_config_json(config: Any, prefix: str = 'config') -> str

Serialise a config dataclass to a temporary JSON file.

Parameters

config : dataclass instance Config object to serialise. prefix : str, optional Filename prefix for the temp file. Default is "config".

Returns

str Absolute path to the created JSON file.

See Also

serialize_config : Convert to dict without writing to disk. read_config_json : Read a JSON config file.

Source code in tit/config_io.py
def write_config_json(config: Any, prefix: str = "config") -> str:
    """Serialise a config dataclass to a temporary JSON file.

    Parameters
    ----------
    config : dataclass instance
        Config object to serialise.
    prefix : str, optional
        Filename prefix for the temp file.  Default is ``"config"``.

    Returns
    -------
    str
        Absolute path to the created JSON file.

    See Also
    --------
    serialize_config : Convert to dict without writing to disk.
    read_config_json : Read a JSON config file.
    """
    data = serialize_config(config)
    fd, path = tempfile.mkstemp(prefix=f"{prefix}_", suffix=".json")
    with os.fdopen(fd, "w") as f:
        json.dump(data, f, indent=2)
    return path

tit.config_io.read_config_json

read_config_json(path: str) -> dict[str, Any]

Read a JSON config file and return the parsed dict.

Parameters

path : str Path to the JSON file.

Returns

dict Parsed JSON contents.

See Also

write_config_json : Create a config JSON file from a dataclass.

Source code in tit/config_io.py
def read_config_json(path: str) -> dict[str, Any]:
    """Read a JSON config file and return the parsed dict.

    Parameters
    ----------
    path : str
        Path to the JSON file.

    Returns
    -------
    dict
        Parsed JSON contents.

    See Also
    --------
    write_config_json : Create a config JSON file from a dataclass.
    """
    with open(path) as f:
        return json.load(f)

Exceptions

tit.pre.utils.PreprocessError

Bases: RuntimeError

Raised when a preprocessing step fails.

See Also

PreprocessCancelled : Raised specifically when cancelled by a stop event.

tit.pre.utils.PreprocessCancelled

Bases: RuntimeError

Raised when a preprocessing run is cancelled via a stop event.

See Also

PreprocessError : General preprocessing failure.

tit.pre.qsi.docker_builder.DockerBuildError

Bases: Exception

Raised when Docker command construction fails.