Skip to content

tit

tit

TI-Toolbox: Temporal Interference Brain Stimulation Platform.

A neuroscience research platform for simulating, optimizing, and analyzing temporal interference (TI) stimulation of the brain. Built on top of SimNIBS for finite-element modeling and FreeSurfer for cortical reconstruction.

Modules

sim TI/mTI simulation engine (electrode montages, field computation). opt Optimization of electrode placements (flex-search, exhaustive). analyzer Field analysis, ROI statistics, and visualization. stats Permutation testing and group-level comparisons. pre Preprocessing pipelines (DICOM conversion, FreeSurfer, CHARM). server FastAPI HTTP server the v3 desktop app (desktop/) drives; it replaced the former PyQt5 interface. cli Command-line tools built on a shared BaseCLI base class. reporting Self-contained HTML reports (DTI QC, simulator, flex-search, ex-search).

Public API

get_path_manager Return the global :class:~tit.paths.PathManager singleton. setup_logging Configure the package-wide logging level. add_file_handler Attach a file handler to a named logger. add_stream_handler Attach a console handler to a named logger. paths BIDS-compliant path resolution module. constants Project-wide constants and configuration values.

Examples

from tit import get_path_manager pm = get_path_manager("/data/project") from tit.sim import SimulationConfig, run_simulation

Notes

Importing this package auto-initializes logging (with stream output at INFO level) and exposes get_path_manager for BIDS path resolution. No additional setup is required for typical usage.

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"
        )

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

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

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