Skip to content

Core (tit.core)

PathManager

PathManager(project_dir: Optional[str] = None)

Centralized path management for TI-Toolbox.

This class provides consistent path resolution across all components: - Project directory detection - Subject listing and validation - Common path patterns (derivatives, SimNIBS, m2m, etc.) - BIDS-compliant directory structure handling

Usage: pm = PathManager() pm.project_dir = "/path/to/project" # Explicit set print(pm.project_dir) # Get current print(pm.project_dir_name) # Just the name

Initialize the path manager.

Args: project_dir: Optional explicit project directory. If not provided, auto-detection from environment variables is attempted on first access of project_dir property.

Source code in tit/core/paths.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def __init__(self, project_dir: Optional[str] = None):
    """
    Initialize the path manager.

    Args:
        project_dir: Optional explicit project directory. If not provided,
                    auto-detection from environment variables is attempted
                    on first access of project_dir property.
    """
    self._project_dir: Optional[str] = None
    _ensure_compiled_templates(self.__class__)

    if project_dir:
        self.project_dir = project_dir  # Use setter for validation

project_dir property writable

project_dir: Optional[str]

Get/set the project directory path.

Auto-detects from environment on first access if not set. Setting validates the path exists.

Usage: pm.project_dir = "/path/to/project" # set path = pm.project_dir # get

project_dir_name property

project_dir_name: Optional[str]

Get the project directory name (basename of project_dir).

analysis_space_dir_name staticmethod

analysis_space_dir_name(space: str) -> str

Map analyzer space ('mesh'|'voxel') to folder name ('Mesh'|'Voxel').

Source code in tit/core/paths.py
529
530
531
532
@staticmethod
def analysis_space_dir_name(space: str) -> str:
    """Map analyzer space ('mesh'|'voxel') to folder name ('Mesh'|'Voxel')."""
    return "Mesh" if str(space).lower() == "mesh" else "Voxel"

cortical_analysis_name classmethod

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

Match GUI/CLI naming for cortical analysis folders.

Source code in tit/core/paths.py
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
@classmethod
def cortical_analysis_name(
    cls,
    *,
    whole_head: bool,
    region: Optional[str],
    atlas_name: Optional[str] = None,
    atlas_path: Optional[str] = None,
) -> str:
    """Match GUI/CLI naming for cortical analysis folders."""
    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")
    return f"region_{region_val}_{atlas_clean}"

ensure_dir

ensure_dir(key: str, /, **kwargs) -> str

Resolve a directory path and create it (parents=True, exist_ok=True).

Source code in tit/core/paths.py
469
470
471
472
473
def ensure_dir(self, key: str, /, **kwargs) -> str:
    """Resolve a directory path and create it (parents=True, exist_ok=True)."""
    p = self.path(key, **kwargs)
    os.makedirs(p, exist_ok=True)
    return p

exists

exists(key: str, /, **kwargs) -> bool

Check if a path exists.

Parameters:

Name Type Description Default
key str

Path template key

required
**kwargs

Template entities

{}

Returns:

Type Description
bool

True if path can be resolved and exists on filesystem

Examples:

>>> pm = PathManager()
>>> if pm.exists("m2m", subject_id="001"):
...     print("m2m directory exists")
Source code in tit/core/paths.py
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
def exists(self, key: str, /, **kwargs) -> bool:
    """
    Check if a path exists.

    Parameters
    ----------
    key : str
        Path template key
    **kwargs
        Template entities

    Returns
    -------
    bool
        True if path can be resolved and exists on filesystem

    Examples
    --------
    >>> pm = PathManager()
    >>> if pm.exists("m2m", subject_id="001"):
    ...     print("m2m directory exists")
    """
    p = self.path_optional(key, **kwargs)
    return bool(p and os.path.exists(p))

get_analysis_output_dir

get_analysis_output_dir(*, subject_id: str, simulation_name: str, space: str, analysis_type: str, coordinates: Optional[List[float]] = None, radius: Optional[float] = None, coordinate_space: str = 'subject', whole_head: bool = False, region: Optional[str] = None, atlas_name: Optional[str] = None, atlas_path: Optional[str] = None) -> Optional[str]

Centralized analysis output directory used by GUI/CLI. Returns the folder but does NOT create it.

Source code in tit/core/paths.py
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
def get_analysis_output_dir(
    self,
    *,
    subject_id: str,
    simulation_name: str,
    space: str,
    analysis_type: str,
    coordinates: Optional[List[float]] = None,
    radius: Optional[float] = None,
    coordinate_space: str = "subject",
    whole_head: bool = False,
    region: Optional[str] = None,
    atlas_name: Optional[str] = None,
    atlas_path: Optional[str] = None,
) -> Optional[str]:
    """
    Centralized analysis output directory used by GUI/CLI.
    Returns the folder but does NOT create it.
    """
    base = self.get_analysis_space_dir(subject_id, simulation_name, space)
    if not base:
        return None

    at = str(analysis_type).lower()
    if at == "spherical":
        if not coordinates or len(coordinates) != 3 or radius is None:
            raise ValueError("coordinates(3) and radius are required for spherical analysis output path")
        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)

get_analysis_space_dir

get_analysis_space_dir(subject_id: str, simulation_name: str, space: str) -> Optional[str]

Get base analysis dir: .../Simulations//Analyses/.

Source code in tit/core/paths.py
569
570
571
572
573
574
def get_analysis_space_dir(self, subject_id: str, simulation_name: str, space: str) -> Optional[str]:
    """Get base analysis dir: .../Simulations/<sim>/Analyses/<Mesh|Voxel>."""
    sim_dir = self.path_optional("simulation", subject_id=subject_id, simulation_name=simulation_name)
    if not sim_dir:
        return None
    return os.path.join(sim_dir, const.DIR_ANALYSIS, self.analysis_space_dir_name(space))

get_derivatives_dir

get_derivatives_dir() -> Optional[str]

Get the derivatives directory path.

Source code in tit/core/paths.py
609
610
611
def get_derivatives_dir(self) -> Optional[str]:
    """Get the derivatives directory path."""
    return self.path_optional("derivatives")

is_dir

is_dir(key: str, /, **kwargs) -> bool

Check if a path exists and is a directory.

Parameters:

Name Type Description Default
key str

Path template key

required
**kwargs

Template entities

{}

Returns:

Type Description
bool

True if path can be resolved and is an existing directory

Examples:

>>> pm = PathManager()
>>> if pm.is_dir("simulations", subject_id="001"):
...     simulations = pm.list_simulations("001")
Source code in tit/core/paths.py
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
def is_dir(self, key: str, /, **kwargs) -> bool:
    """
    Check if a path exists and is a directory.

    Parameters
    ----------
    key : str
        Path template key
    **kwargs
        Template entities

    Returns
    -------
    bool
        True if path can be resolved and is an existing directory

    Examples
    --------
    >>> pm = PathManager()
    >>> if pm.is_dir("simulations", subject_id="001"):
    ...     simulations = pm.list_simulations("001")
    """
    p = self.path_optional(key, **kwargs)
    return bool(p and os.path.isdir(p))

is_file

is_file(key: str, /, **kwargs) -> bool

Check if a path exists and is a file.

Parameters:

Name Type Description Default
key str

Path template key

required
**kwargs

Template entities

{}

Returns:

Type Description
bool

True if path can be resolved and is an existing file

Examples:

>>> pm = PathManager()
>>> if pm.is_file("ti_mesh", subject_id="001", simulation_name="montage1"):
...     print("TI mesh file exists")
Source code in tit/core/paths.py
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
def is_file(self, key: str, /, **kwargs) -> bool:
    """
    Check if a path exists and is a file.

    Parameters
    ----------
    key : str
        Path template key
    **kwargs
        Template entities

    Returns
    -------
    bool
        True if path can be resolved and is an existing file

    Examples
    --------
    >>> pm = PathManager()
    >>> if pm.is_file("ti_mesh", subject_id="001", simulation_name="montage1"):
    ...     print("TI mesh file exists")
    """
    p = self.path_optional(key, **kwargs)
    return bool(p and os.path.isfile(p))

list_eeg_caps

list_eeg_caps(subject_id: str) -> List[str]

List available EEG cap files for a subject.

Args: subject_id: Subject ID

Returns: List of EEG cap CSV filenames, sorted alphabetically

Source code in tit/core/paths.py
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
def list_eeg_caps(self, subject_id: str) -> List[str]:
    """
    List available EEG cap files for a subject.

    Args:
        subject_id: Subject ID

    Returns:
        List of EEG cap CSV filenames, sorted alphabetically
    """
    eeg_pos_dir = self.path_optional("eeg_positions", subject_id=subject_id)
    if not eeg_pos_dir or not os.path.isdir(eeg_pos_dir):
        return []

    caps = []
    for file in os.listdir(eeg_pos_dir):
        if file.endswith(const.EXT_CSV) and not file.startswith('.'):
            caps.append(file)

    caps.sort()
    return caps

list_flex_search_runs

list_flex_search_runs(subject_id: str) -> List[str]

List flex-search run folders that contain electrode_positions.json. Uses os.scandir for efficiency.

Source code in tit/core/paths.py
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
def list_flex_search_runs(self, subject_id: str) -> List[str]:
    """
    List flex-search run folders that contain electrode_positions.json.
    Uses os.scandir for efficiency.
    """
    root = self.path_optional("flex_search", subject_id=subject_id)
    if not root or not os.path.isdir(root):
        return []
    out: List[str] = []
    fname = "electrode_positions.json"
    try:
        with os.scandir(root) as it:
            for entry in it:
                if not entry.is_dir():
                    continue
                if entry.name.startswith("."):
                    continue
                if os.path.isfile(os.path.join(entry.path, fname)):
                    out.append(entry.name)
    except OSError:
        return []
    out.sort()
    return out

list_simulations

list_simulations(subject_id: str) -> List[str]

List all simulations for a subject.

Args: subject_id: Subject ID

Returns: List of simulation names, sorted alphabetically

Source code in tit/core/paths.py
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
def list_simulations(self, subject_id: str) -> List[str]:
    """
    List all simulations for a subject.

    Args:
        subject_id: Subject ID

    Returns:
        List of simulation names, sorted alphabetically
    """
    sim_root = self.path_optional("simulations", subject_id=subject_id)
    if not sim_root or not os.path.isdir(sim_root):
        return []

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

list_subjects

list_subjects() -> List[str]

List all available subjects in the project.

Returns: List of subject IDs (without 'sub-' prefix), sorted naturally

Source code in tit/core/paths.py
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
def list_subjects(self) -> List[str]:
    """
    List all available subjects in the project.

    Returns:
        List of subject IDs (without 'sub-' prefix), sorted naturally
    """
    simnibs_dir = self.path_optional("simnibs")
    if not simnibs_dir or not os.path.isdir(simnibs_dir):
        return []

    subjects = []
    for item in os.listdir(simnibs_dir):
        if not item.startswith(const.PREFIX_SUBJECT):
            continue
        subject_id = item.replace(const.PREFIX_SUBJECT, "")
        # Only list subjects that have an m2m folder (SimNIBS-ready).
        if self.is_dir("m2m", subject_id=subject_id):
            subjects.append(subject_id)

    # Sort subjects naturally (001, 002, 010, 100)
    subjects.sort(key=lambda x: [int(c) if c.isdigit() else c.lower() 
                                 for c in re.split('([0-9]+)', x)])
    return subjects

path

path(key: str, /, **kwargs) -> str

Resolve a canonical path by key from predefined templates.

This is the primary path resolution method. It provides fast, cached access to all standard TI-Toolbox paths using pre-compiled templates.

Parameters:

Name Type Description Default
key str

Path template key (e.g., 'm2m', 'simulation', 'ti_mesh')

required
**kwargs

Required entities for the template (e.g., subject_id='001', simulation_name='montage1')

{}

Returns:

Type Description
str

Resolved absolute path

Raises:

Type Description
RuntimeError

If project_dir is not resolved

KeyError

If key is unknown

ValueError

If required template entities are missing

Examples:

>>> pm = PathManager()
>>> m2m_path = pm.path("m2m", subject_id="001")
>>> sim_path = pm.path("simulation", subject_id="001", simulation_name="montage1")
>>> mesh_path = pm.path("ti_mesh", subject_id="001", simulation_name="montage1")
Notes
  • Results are cached for performance (8192 entry LRU cache)
  • All paths are resolved relative to project_dir
  • Template entities are type-checked and missing entities raise ValueError
Source code in tit/core/paths.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def path(self, key: str, /, **kwargs) -> str:
    """
    Resolve a canonical path by key from predefined templates.

    This is the primary path resolution method. It provides fast, cached access
    to all standard TI-Toolbox paths using pre-compiled templates.

    Parameters
    ----------
    key : str
        Path template key (e.g., 'm2m', 'simulation', 'ti_mesh')
    **kwargs
        Required entities for the template (e.g., subject_id='001', simulation_name='montage1')

    Returns
    -------
    str
        Resolved absolute path

    Raises
    ------
    RuntimeError
        If project_dir is not resolved
    KeyError
        If key is unknown
    ValueError
        If required template entities are missing

    Examples
    --------
    >>> pm = PathManager()
    >>> m2m_path = pm.path("m2m", subject_id="001")
    >>> sim_path = pm.path("simulation", subject_id="001", simulation_name="montage1")
    >>> mesh_path = pm.path("ti_mesh", subject_id="001", simulation_name="montage1")

    Notes
    -----
    - Results are cached for performance (8192 entry LRU cache)
    - All paths are resolved relative to project_dir
    - Template entities are type-checked and missing entities raise ValueError
    """
    if not self.project_dir:
        raise RuntimeError("Project directory not resolved. Set PROJECT_DIR_NAME or PROJECT_DIR in Docker.")
    tpl = _COMPILED_TEMPLATES.get(key)
    if tpl is None:
        raise KeyError(f"Unknown path key: {key}")
    missing = [e for e in tpl.required_entities if e not in kwargs]
    if missing:
        raise ValueError(f"Missing required path entities for {key!r}: {', '.join(missing)}")
    return _cached_render(self.project_dir, key, _freeze_kwargs(kwargs))

path_optional

path_optional(key: str, /, **kwargs) -> Optional[str]

Resolve a path without raising exceptions.

Similar to path() but returns None instead of raising exceptions. Useful for checking if paths exist or can be resolved without error handling.

Parameters:

Name Type Description Default
key str

Path template key

required
**kwargs

Template entities (e.g., subject_id='001')

{}

Returns:

Type Description
str or None

Resolved path if successful, None otherwise

Examples:

>>> pm = PathManager()
>>> m2m_path = pm.path_optional("m2m", subject_id="001")
>>> if m2m_path:
...     print(f"m2m exists at {m2m_path}")
Notes

Returns None if: - project_dir is not resolved - key is unknown - required entities are missing

Source code in tit/core/paths.py
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
def path_optional(self, key: str, /, **kwargs) -> Optional[str]:
    """
    Resolve a path without raising exceptions.

    Similar to path() but returns None instead of raising exceptions.
    Useful for checking if paths exist or can be resolved without error handling.

    Parameters
    ----------
    key : str
        Path template key
    **kwargs
        Template entities (e.g., subject_id='001')

    Returns
    -------
    str or None
        Resolved path if successful, None otherwise

    Examples
    --------
    >>> pm = PathManager()
    >>> m2m_path = pm.path_optional("m2m", subject_id="001")
    >>> if m2m_path:
    ...     print(f"m2m exists at {m2m_path}")

    Notes
    -----
    Returns None if:
    - project_dir is not resolved
    - key is unknown
    - required entities are missing
    """
    if not self.project_dir:
        return None
    tpl = _COMPILED_TEMPLATES.get(key)
    if tpl is None:
        return None
    # Optional resolver: if required entities are missing, return None.
    for e in tpl.required_entities:
        if e not in kwargs:
            return None
    try:
        return _cached_render(self.project_dir, key, _freeze_kwargs(kwargs))
    except KeyError:
        return None

spherical_analysis_name staticmethod

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

Match GUI/CLI naming: sphere_x..y.._z.._r..{_MNI|_subject}.

Source code in tit/core/paths.py
545
546
547
548
549
@staticmethod
def spherical_analysis_name(x: float, y: float, z: float, radius: float, coordinate_space: str) -> str:
    """Match GUI/CLI naming: sphere_x.._y.._z.._r.._{_MNI|_subject}."""
    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}"

validate_subject_structure

validate_subject_structure(subject_id: str) -> Dict[str, any]

Validate that a subject has the required directory structure.

Args: subject_id: Subject ID

Returns: Dictionary with validation results: - 'valid': bool indicating if structure is valid - 'missing': list of missing required components - 'warnings': list of optional missing components

Source code in tit/core/paths.py
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
def validate_subject_structure(self, subject_id: str) -> Dict[str, any]:
    """
    Validate that a subject has the required directory structure.

    Args:
        subject_id: Subject ID

    Returns:
        Dictionary with validation results:
        - 'valid': bool indicating if structure is valid
        - 'missing': list of missing required components
        - 'warnings': list of optional missing components
    """
    results = {
        'valid': True,
        'missing': [],
        'warnings': []
    }

    # Check subject directory
    subject_dir = self.path_optional("simnibs_subject", subject_id=subject_id)
    if not subject_dir or not os.path.isdir(subject_dir):
        results['valid'] = False
        results['missing'].append(f"Subject directory: {const.PREFIX_SUBJECT}{subject_id}")
        return results

    # Check m2m directory
    if not self.is_dir("m2m", subject_id=subject_id):
        results['valid'] = False
        results['missing'].append(f"m2m directory: {const.DIR_M2M_PREFIX}{subject_id}")

    # Check for EEG positions (optional warning)
    if not self.is_dir("eeg_positions", subject_id=subject_id):
        results['warnings'].append(const.WARNING_NO_EEG_POSITIONS)

    return results

get_path_manager

get_path_manager() -> PathManager

Get the global PathManager singleton instance.

Returns: The global path manager instance

Source code in tit/core/paths.py
680
681
682
683
684
685
686
687
688
689
690
def get_path_manager() -> PathManager:
    """
    Get the global PathManager singleton instance.

    Returns:
        The global path manager instance
    """
    global _path_manager_instance
    if _path_manager_instance is None:
        _path_manager_instance = PathManager()
    return _path_manager_instance

reset_path_manager

reset_path_manager()

Reset the global PathManager singleton instance. Useful for testing or when project directory changes.

Source code in tit/core/paths.py
693
694
695
696
697
698
699
700
701
702
703
def reset_path_manager():
    """
    Reset the global PathManager singleton instance.
    Useful for testing or when project directory changes.
    """
    global _path_manager_instance
    _path_manager_instance = None
    try:
        _cached_render.cache_clear()
    except Exception:
        pass

Paths (tit.core.paths)

TI-Toolbox Path Management Centralized path management for the entire TI-Toolbox codebase.

This module provides a professional path management system for BIDS-compliant directory structures, handling project directories, subject paths, and common path patterns consistently across all tools.

Usage: # Use the singleton instance from tit.core import get_path_manager

pm = get_path_manager()
subjects = pm.list_subjects()
m2m_dir = pm.path("m2m", subject_id="001")
sim_dir = pm.path("simulation", subject_id="001", simulation_name="montage1")

PathManager

PathManager(project_dir: Optional[str] = None)

Centralized path management for TI-Toolbox.

This class provides consistent path resolution across all components: - Project directory detection - Subject listing and validation - Common path patterns (derivatives, SimNIBS, m2m, etc.) - BIDS-compliant directory structure handling

Usage: pm = PathManager() pm.project_dir = "/path/to/project" # Explicit set print(pm.project_dir) # Get current print(pm.project_dir_name) # Just the name

Initialize the path manager.

Args: project_dir: Optional explicit project directory. If not provided, auto-detection from environment variables is attempted on first access of project_dir property.

Source code in tit/core/paths.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def __init__(self, project_dir: Optional[str] = None):
    """
    Initialize the path manager.

    Args:
        project_dir: Optional explicit project directory. If not provided,
                    auto-detection from environment variables is attempted
                    on first access of project_dir property.
    """
    self._project_dir: Optional[str] = None
    _ensure_compiled_templates(self.__class__)

    if project_dir:
        self.project_dir = project_dir  # Use setter for validation

project_dir property writable

project_dir: Optional[str]

Get/set the project directory path.

Auto-detects from environment on first access if not set. Setting validates the path exists.

Usage: pm.project_dir = "/path/to/project" # set path = pm.project_dir # get

project_dir_name property

project_dir_name: Optional[str]

Get the project directory name (basename of project_dir).

analysis_space_dir_name staticmethod

analysis_space_dir_name(space: str) -> str

Map analyzer space ('mesh'|'voxel') to folder name ('Mesh'|'Voxel').

Source code in tit/core/paths.py
529
530
531
532
@staticmethod
def analysis_space_dir_name(space: str) -> str:
    """Map analyzer space ('mesh'|'voxel') to folder name ('Mesh'|'Voxel')."""
    return "Mesh" if str(space).lower() == "mesh" else "Voxel"

cortical_analysis_name classmethod

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

Match GUI/CLI naming for cortical analysis folders.

Source code in tit/core/paths.py
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
@classmethod
def cortical_analysis_name(
    cls,
    *,
    whole_head: bool,
    region: Optional[str],
    atlas_name: Optional[str] = None,
    atlas_path: Optional[str] = None,
) -> str:
    """Match GUI/CLI naming for cortical analysis folders."""
    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")
    return f"region_{region_val}_{atlas_clean}"

ensure_dir

ensure_dir(key: str, /, **kwargs) -> str

Resolve a directory path and create it (parents=True, exist_ok=True).

Source code in tit/core/paths.py
469
470
471
472
473
def ensure_dir(self, key: str, /, **kwargs) -> str:
    """Resolve a directory path and create it (parents=True, exist_ok=True)."""
    p = self.path(key, **kwargs)
    os.makedirs(p, exist_ok=True)
    return p

exists

exists(key: str, /, **kwargs) -> bool

Check if a path exists.

Parameters:

Name Type Description Default
key str

Path template key

required
**kwargs

Template entities

{}

Returns:

Type Description
bool

True if path can be resolved and exists on filesystem

Examples:

>>> pm = PathManager()
>>> if pm.exists("m2m", subject_id="001"):
...     print("m2m directory exists")
Source code in tit/core/paths.py
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
def exists(self, key: str, /, **kwargs) -> bool:
    """
    Check if a path exists.

    Parameters
    ----------
    key : str
        Path template key
    **kwargs
        Template entities

    Returns
    -------
    bool
        True if path can be resolved and exists on filesystem

    Examples
    --------
    >>> pm = PathManager()
    >>> if pm.exists("m2m", subject_id="001"):
    ...     print("m2m directory exists")
    """
    p = self.path_optional(key, **kwargs)
    return bool(p and os.path.exists(p))

get_analysis_output_dir

get_analysis_output_dir(*, subject_id: str, simulation_name: str, space: str, analysis_type: str, coordinates: Optional[List[float]] = None, radius: Optional[float] = None, coordinate_space: str = 'subject', whole_head: bool = False, region: Optional[str] = None, atlas_name: Optional[str] = None, atlas_path: Optional[str] = None) -> Optional[str]

Centralized analysis output directory used by GUI/CLI. Returns the folder but does NOT create it.

Source code in tit/core/paths.py
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
def get_analysis_output_dir(
    self,
    *,
    subject_id: str,
    simulation_name: str,
    space: str,
    analysis_type: str,
    coordinates: Optional[List[float]] = None,
    radius: Optional[float] = None,
    coordinate_space: str = "subject",
    whole_head: bool = False,
    region: Optional[str] = None,
    atlas_name: Optional[str] = None,
    atlas_path: Optional[str] = None,
) -> Optional[str]:
    """
    Centralized analysis output directory used by GUI/CLI.
    Returns the folder but does NOT create it.
    """
    base = self.get_analysis_space_dir(subject_id, simulation_name, space)
    if not base:
        return None

    at = str(analysis_type).lower()
    if at == "spherical":
        if not coordinates or len(coordinates) != 3 or radius is None:
            raise ValueError("coordinates(3) and radius are required for spherical analysis output path")
        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)

get_analysis_space_dir

get_analysis_space_dir(subject_id: str, simulation_name: str, space: str) -> Optional[str]

Get base analysis dir: .../Simulations//Analyses/.

Source code in tit/core/paths.py
569
570
571
572
573
574
def get_analysis_space_dir(self, subject_id: str, simulation_name: str, space: str) -> Optional[str]:
    """Get base analysis dir: .../Simulations/<sim>/Analyses/<Mesh|Voxel>."""
    sim_dir = self.path_optional("simulation", subject_id=subject_id, simulation_name=simulation_name)
    if not sim_dir:
        return None
    return os.path.join(sim_dir, const.DIR_ANALYSIS, self.analysis_space_dir_name(space))

get_derivatives_dir

get_derivatives_dir() -> Optional[str]

Get the derivatives directory path.

Source code in tit/core/paths.py
609
610
611
def get_derivatives_dir(self) -> Optional[str]:
    """Get the derivatives directory path."""
    return self.path_optional("derivatives")

is_dir

is_dir(key: str, /, **kwargs) -> bool

Check if a path exists and is a directory.

Parameters:

Name Type Description Default
key str

Path template key

required
**kwargs

Template entities

{}

Returns:

Type Description
bool

True if path can be resolved and is an existing directory

Examples:

>>> pm = PathManager()
>>> if pm.is_dir("simulations", subject_id="001"):
...     simulations = pm.list_simulations("001")
Source code in tit/core/paths.py
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
def is_dir(self, key: str, /, **kwargs) -> bool:
    """
    Check if a path exists and is a directory.

    Parameters
    ----------
    key : str
        Path template key
    **kwargs
        Template entities

    Returns
    -------
    bool
        True if path can be resolved and is an existing directory

    Examples
    --------
    >>> pm = PathManager()
    >>> if pm.is_dir("simulations", subject_id="001"):
    ...     simulations = pm.list_simulations("001")
    """
    p = self.path_optional(key, **kwargs)
    return bool(p and os.path.isdir(p))

is_file

is_file(key: str, /, **kwargs) -> bool

Check if a path exists and is a file.

Parameters:

Name Type Description Default
key str

Path template key

required
**kwargs

Template entities

{}

Returns:

Type Description
bool

True if path can be resolved and is an existing file

Examples:

>>> pm = PathManager()
>>> if pm.is_file("ti_mesh", subject_id="001", simulation_name="montage1"):
...     print("TI mesh file exists")
Source code in tit/core/paths.py
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
def is_file(self, key: str, /, **kwargs) -> bool:
    """
    Check if a path exists and is a file.

    Parameters
    ----------
    key : str
        Path template key
    **kwargs
        Template entities

    Returns
    -------
    bool
        True if path can be resolved and is an existing file

    Examples
    --------
    >>> pm = PathManager()
    >>> if pm.is_file("ti_mesh", subject_id="001", simulation_name="montage1"):
    ...     print("TI mesh file exists")
    """
    p = self.path_optional(key, **kwargs)
    return bool(p and os.path.isfile(p))

list_eeg_caps

list_eeg_caps(subject_id: str) -> List[str]

List available EEG cap files for a subject.

Args: subject_id: Subject ID

Returns: List of EEG cap CSV filenames, sorted alphabetically

Source code in tit/core/paths.py
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
def list_eeg_caps(self, subject_id: str) -> List[str]:
    """
    List available EEG cap files for a subject.

    Args:
        subject_id: Subject ID

    Returns:
        List of EEG cap CSV filenames, sorted alphabetically
    """
    eeg_pos_dir = self.path_optional("eeg_positions", subject_id=subject_id)
    if not eeg_pos_dir or not os.path.isdir(eeg_pos_dir):
        return []

    caps = []
    for file in os.listdir(eeg_pos_dir):
        if file.endswith(const.EXT_CSV) and not file.startswith('.'):
            caps.append(file)

    caps.sort()
    return caps

list_flex_search_runs

list_flex_search_runs(subject_id: str) -> List[str]

List flex-search run folders that contain electrode_positions.json. Uses os.scandir for efficiency.

Source code in tit/core/paths.py
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
def list_flex_search_runs(self, subject_id: str) -> List[str]:
    """
    List flex-search run folders that contain electrode_positions.json.
    Uses os.scandir for efficiency.
    """
    root = self.path_optional("flex_search", subject_id=subject_id)
    if not root or not os.path.isdir(root):
        return []
    out: List[str] = []
    fname = "electrode_positions.json"
    try:
        with os.scandir(root) as it:
            for entry in it:
                if not entry.is_dir():
                    continue
                if entry.name.startswith("."):
                    continue
                if os.path.isfile(os.path.join(entry.path, fname)):
                    out.append(entry.name)
    except OSError:
        return []
    out.sort()
    return out

list_simulations

list_simulations(subject_id: str) -> List[str]

List all simulations for a subject.

Args: subject_id: Subject ID

Returns: List of simulation names, sorted alphabetically

Source code in tit/core/paths.py
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
def list_simulations(self, subject_id: str) -> List[str]:
    """
    List all simulations for a subject.

    Args:
        subject_id: Subject ID

    Returns:
        List of simulation names, sorted alphabetically
    """
    sim_root = self.path_optional("simulations", subject_id=subject_id)
    if not sim_root or not os.path.isdir(sim_root):
        return []

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

list_subjects

list_subjects() -> List[str]

List all available subjects in the project.

Returns: List of subject IDs (without 'sub-' prefix), sorted naturally

Source code in tit/core/paths.py
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
def list_subjects(self) -> List[str]:
    """
    List all available subjects in the project.

    Returns:
        List of subject IDs (without 'sub-' prefix), sorted naturally
    """
    simnibs_dir = self.path_optional("simnibs")
    if not simnibs_dir or not os.path.isdir(simnibs_dir):
        return []

    subjects = []
    for item in os.listdir(simnibs_dir):
        if not item.startswith(const.PREFIX_SUBJECT):
            continue
        subject_id = item.replace(const.PREFIX_SUBJECT, "")
        # Only list subjects that have an m2m folder (SimNIBS-ready).
        if self.is_dir("m2m", subject_id=subject_id):
            subjects.append(subject_id)

    # Sort subjects naturally (001, 002, 010, 100)
    subjects.sort(key=lambda x: [int(c) if c.isdigit() else c.lower() 
                                 for c in re.split('([0-9]+)', x)])
    return subjects

path

path(key: str, /, **kwargs) -> str

Resolve a canonical path by key from predefined templates.

This is the primary path resolution method. It provides fast, cached access to all standard TI-Toolbox paths using pre-compiled templates.

Parameters:

Name Type Description Default
key str

Path template key (e.g., 'm2m', 'simulation', 'ti_mesh')

required
**kwargs

Required entities for the template (e.g., subject_id='001', simulation_name='montage1')

{}

Returns:

Type Description
str

Resolved absolute path

Raises:

Type Description
RuntimeError

If project_dir is not resolved

KeyError

If key is unknown

ValueError

If required template entities are missing

Examples:

>>> pm = PathManager()
>>> m2m_path = pm.path("m2m", subject_id="001")
>>> sim_path = pm.path("simulation", subject_id="001", simulation_name="montage1")
>>> mesh_path = pm.path("ti_mesh", subject_id="001", simulation_name="montage1")
Notes
  • Results are cached for performance (8192 entry LRU cache)
  • All paths are resolved relative to project_dir
  • Template entities are type-checked and missing entities raise ValueError
Source code in tit/core/paths.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def path(self, key: str, /, **kwargs) -> str:
    """
    Resolve a canonical path by key from predefined templates.

    This is the primary path resolution method. It provides fast, cached access
    to all standard TI-Toolbox paths using pre-compiled templates.

    Parameters
    ----------
    key : str
        Path template key (e.g., 'm2m', 'simulation', 'ti_mesh')
    **kwargs
        Required entities for the template (e.g., subject_id='001', simulation_name='montage1')

    Returns
    -------
    str
        Resolved absolute path

    Raises
    ------
    RuntimeError
        If project_dir is not resolved
    KeyError
        If key is unknown
    ValueError
        If required template entities are missing

    Examples
    --------
    >>> pm = PathManager()
    >>> m2m_path = pm.path("m2m", subject_id="001")
    >>> sim_path = pm.path("simulation", subject_id="001", simulation_name="montage1")
    >>> mesh_path = pm.path("ti_mesh", subject_id="001", simulation_name="montage1")

    Notes
    -----
    - Results are cached for performance (8192 entry LRU cache)
    - All paths are resolved relative to project_dir
    - Template entities are type-checked and missing entities raise ValueError
    """
    if not self.project_dir:
        raise RuntimeError("Project directory not resolved. Set PROJECT_DIR_NAME or PROJECT_DIR in Docker.")
    tpl = _COMPILED_TEMPLATES.get(key)
    if tpl is None:
        raise KeyError(f"Unknown path key: {key}")
    missing = [e for e in tpl.required_entities if e not in kwargs]
    if missing:
        raise ValueError(f"Missing required path entities for {key!r}: {', '.join(missing)}")
    return _cached_render(self.project_dir, key, _freeze_kwargs(kwargs))

path_optional

path_optional(key: str, /, **kwargs) -> Optional[str]

Resolve a path without raising exceptions.

Similar to path() but returns None instead of raising exceptions. Useful for checking if paths exist or can be resolved without error handling.

Parameters:

Name Type Description Default
key str

Path template key

required
**kwargs

Template entities (e.g., subject_id='001')

{}

Returns:

Type Description
str or None

Resolved path if successful, None otherwise

Examples:

>>> pm = PathManager()
>>> m2m_path = pm.path_optional("m2m", subject_id="001")
>>> if m2m_path:
...     print(f"m2m exists at {m2m_path}")
Notes

Returns None if: - project_dir is not resolved - key is unknown - required entities are missing

Source code in tit/core/paths.py
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
def path_optional(self, key: str, /, **kwargs) -> Optional[str]:
    """
    Resolve a path without raising exceptions.

    Similar to path() but returns None instead of raising exceptions.
    Useful for checking if paths exist or can be resolved without error handling.

    Parameters
    ----------
    key : str
        Path template key
    **kwargs
        Template entities (e.g., subject_id='001')

    Returns
    -------
    str or None
        Resolved path if successful, None otherwise

    Examples
    --------
    >>> pm = PathManager()
    >>> m2m_path = pm.path_optional("m2m", subject_id="001")
    >>> if m2m_path:
    ...     print(f"m2m exists at {m2m_path}")

    Notes
    -----
    Returns None if:
    - project_dir is not resolved
    - key is unknown
    - required entities are missing
    """
    if not self.project_dir:
        return None
    tpl = _COMPILED_TEMPLATES.get(key)
    if tpl is None:
        return None
    # Optional resolver: if required entities are missing, return None.
    for e in tpl.required_entities:
        if e not in kwargs:
            return None
    try:
        return _cached_render(self.project_dir, key, _freeze_kwargs(kwargs))
    except KeyError:
        return None

spherical_analysis_name staticmethod

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

Match GUI/CLI naming: sphere_x..y.._z.._r..{_MNI|_subject}.

Source code in tit/core/paths.py
545
546
547
548
549
@staticmethod
def spherical_analysis_name(x: float, y: float, z: float, radius: float, coordinate_space: str) -> str:
    """Match GUI/CLI naming: sphere_x.._y.._z.._r.._{_MNI|_subject}."""
    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}"

validate_subject_structure

validate_subject_structure(subject_id: str) -> Dict[str, any]

Validate that a subject has the required directory structure.

Args: subject_id: Subject ID

Returns: Dictionary with validation results: - 'valid': bool indicating if structure is valid - 'missing': list of missing required components - 'warnings': list of optional missing components

Source code in tit/core/paths.py
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
def validate_subject_structure(self, subject_id: str) -> Dict[str, any]:
    """
    Validate that a subject has the required directory structure.

    Args:
        subject_id: Subject ID

    Returns:
        Dictionary with validation results:
        - 'valid': bool indicating if structure is valid
        - 'missing': list of missing required components
        - 'warnings': list of optional missing components
    """
    results = {
        'valid': True,
        'missing': [],
        'warnings': []
    }

    # Check subject directory
    subject_dir = self.path_optional("simnibs_subject", subject_id=subject_id)
    if not subject_dir or not os.path.isdir(subject_dir):
        results['valid'] = False
        results['missing'].append(f"Subject directory: {const.PREFIX_SUBJECT}{subject_id}")
        return results

    # Check m2m directory
    if not self.is_dir("m2m", subject_id=subject_id):
        results['valid'] = False
        results['missing'].append(f"m2m directory: {const.DIR_M2M_PREFIX}{subject_id}")

    # Check for EEG positions (optional warning)
    if not self.is_dir("eeg_positions", subject_id=subject_id):
        results['warnings'].append(const.WARNING_NO_EEG_POSITIONS)

    return results

get_path_manager

get_path_manager() -> PathManager

Get the global PathManager singleton instance.

Returns: The global path manager instance

Source code in tit/core/paths.py
680
681
682
683
684
685
686
687
688
689
690
def get_path_manager() -> PathManager:
    """
    Get the global PathManager singleton instance.

    Returns:
        The global path manager instance
    """
    global _path_manager_instance
    if _path_manager_instance is None:
        _path_manager_instance = PathManager()
    return _path_manager_instance

reset_path_manager

reset_path_manager()

Reset the global PathManager singleton instance. Useful for testing or when project directory changes.

Source code in tit/core/paths.py
693
694
695
696
697
698
699
700
701
702
703
def reset_path_manager():
    """
    Reset the global PathManager singleton instance.
    Useful for testing or when project directory changes.
    """
    global _path_manager_instance
    _path_manager_instance = None
    try:
        _cached_render.cache_clear()
    except Exception:
        pass

Constants (tit.core.constants)

TI-Toolbox Constants Centralized constants for the entire TI-Toolbox codebase. This module contains all hardcoded values, magic numbers, and configuration constants.

NIfTI (tit.core.nifti)

TI-Toolbox NIfTI Module

TI-Toolbox specific NIfTI file operations. Provides functions for loading subject and group data from TI-Toolbox BIDS structure.

load_group_data_ti_toolbox

load_group_data_ti_toolbox(subject_configs: List[Dict], nifti_file_pattern: str = 'grey_{simulation_name}_TI_MNI_MNI_TI_max.nii.gz', dtype=np.float32) -> Tuple[np.ndarray, nib.Nifti1Image, List[str]]

Load multiple subjects from TI-Toolbox BIDS structure

Parameters:

subject_configs : list of dict List of subject configurations with keys: - 'subject_id': Subject ID (e.g., '070') - 'simulation_name': Simulation name (e.g., 'ICP_RHIPPO') nifti_file_pattern : str, optional Pattern for NIfTI files dtype : numpy dtype, optional Data type to load (default: float32)

Returns:

data_4d : ndarray (x, y, z, n_subjects) 4D array with all loaded data template_img : nibabel Nifti1Image Template image from first subject subject_ids : list of str List of successfully loaded subject IDs

Source code in tit/core/nifti.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def load_group_data_ti_toolbox(
    subject_configs: List[Dict],
    nifti_file_pattern: str = "grey_{simulation_name}_TI_MNI_MNI_TI_max.nii.gz",
    dtype=np.float32
) -> Tuple[np.ndarray, nib.Nifti1Image, List[str]]:
    """
    Load multiple subjects from TI-Toolbox BIDS structure

    Parameters:
    -----------
    subject_configs : list of dict
        List of subject configurations with keys:
        - 'subject_id': Subject ID (e.g., '070')
        - 'simulation_name': Simulation name (e.g., 'ICP_RHIPPO')
    nifti_file_pattern : str, optional
        Pattern for NIfTI files
    dtype : numpy dtype, optional
        Data type to load (default: float32)

    Returns:
    --------
    data_4d : ndarray (x, y, z, n_subjects)
        4D array with all loaded data
    template_img : nibabel Nifti1Image
        Template image from first subject
    subject_ids : list of str
        List of successfully loaded subject IDs
    """
    data_list = []
    subject_ids = []
    template_img = None
    template_affine = None
    template_header = None

    for config in subject_configs:
        subject_id = config['subject_id']
        simulation_name = config['simulation_name']

        try:
            data, img, filepath = load_subject_nifti_ti_toolbox(
                subject_id,
                simulation_name,
                nifti_file_pattern,
                dtype=dtype
            )

            # Store template image from first subject
            if template_img is None:
                template_img = img
                template_affine = img.affine.copy()
                template_header = img.header.copy()

            data_list.append(data)
            subject_ids.append(subject_id)

            # Clear the image object to free memory
            del img

        except FileNotFoundError as e:
            print(f"Warning: File not found for subject {subject_id} - {e}")
            continue
        except Exception as e:
            print(f"Warning: Error loading subject {subject_id} - {e}")
            continue

    if len(data_list) == 0:
        raise ValueError("No subjects could be loaded successfully")

    # Stack into 4D array
    data_4d = np.stack(data_list, axis=-1).astype(dtype)

    # Recreate minimal template image
    template_img = nib.Nifti1Image(data_4d[..., 0], template_affine, template_header)

    # Clean up
    del data_list
    gc.collect()

    return data_4d, template_img, subject_ids

load_grouped_subjects_ti_toolbox

load_grouped_subjects_ti_toolbox(subject_configs: List[Dict], nifti_file_pattern: str = 'grey_{simulation_name}_TI_MNI_MNI_TI_max.nii.gz', dtype=np.float32) -> Tuple[Dict[str, np.ndarray], nib.Nifti1Image, Dict[str, List[str]]]

Load subjects organized by groups from TI-Toolbox BIDS structure

Parameters:

subject_configs : list of dict List of subject configurations with keys: - 'subject_id': Subject ID (e.g., '070') - 'simulation_name': Simulation name (e.g., 'ICP_RHIPPO') - 'group': Group name (e.g., 'group1', 'Responders', etc.) nifti_file_pattern : str, optional Pattern for NIfTI files dtype : numpy dtype, optional Data type to load (default: float32)

Returns:

groups_data : dict of str -> ndarray Dictionary mapping group names to 4D arrays (x, y, z, n_subjects) template_img : nibabel Nifti1Image Template image from first subject groups_ids : dict of str -> list of str Dictionary mapping group names to lists of subject IDs

Source code in tit/core/nifti.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def load_grouped_subjects_ti_toolbox(
    subject_configs: List[Dict],
    nifti_file_pattern: str = "grey_{simulation_name}_TI_MNI_MNI_TI_max.nii.gz",
    dtype=np.float32
) -> Tuple[Dict[str, np.ndarray], nib.Nifti1Image, Dict[str, List[str]]]:
    """
    Load subjects organized by groups from TI-Toolbox BIDS structure

    Parameters:
    -----------
    subject_configs : list of dict
        List of subject configurations with keys:
        - 'subject_id': Subject ID (e.g., '070')
        - 'simulation_name': Simulation name (e.g., 'ICP_RHIPPO')
        - 'group': Group name (e.g., 'group1', 'Responders', etc.)
    nifti_file_pattern : str, optional
        Pattern for NIfTI files
    dtype : numpy dtype, optional
        Data type to load (default: float32)

    Returns:
    --------
    groups_data : dict of str -> ndarray
        Dictionary mapping group names to 4D arrays (x, y, z, n_subjects)
    template_img : nibabel Nifti1Image
        Template image from first subject
    groups_ids : dict of str -> list of str
        Dictionary mapping group names to lists of subject IDs
    """
    # Organize configs by group
    groups = {}
    for config in subject_configs:
        group_name = config.get('group', 'default')
        if group_name not in groups:
            groups[group_name] = []
        groups[group_name].append(config)

    # Load each group
    groups_data = {}
    groups_ids = {}
    template_img = None

    for group_name, group_configs in groups.items():
        data_4d, img, subject_ids = load_group_data_ti_toolbox(
            group_configs,
            nifti_file_pattern,
            dtype=dtype
        )

        groups_data[group_name] = data_4d
        groups_ids[group_name] = subject_ids

        # Use first group's image as template
        if template_img is None:
            template_img = img

    return groups_data, template_img, groups_ids

load_subject_nifti_ti_toolbox

load_subject_nifti_ti_toolbox(subject_id: str, simulation_name: str, nifti_file_pattern: str = 'grey_{simulation_name}_TI_MNI_MNI_TI_max.nii.gz', dtype=np.float32) -> Tuple[np.ndarray, nib.Nifti1Image, str]

Load a NIfTI file from TI-Toolbox BIDS structure

Parameters:

subject_id : str Subject ID (e.g., '070') simulation_name : str Simulation name (e.g., 'ICP_RHIPPO') nifti_file_pattern : str, optional Pattern for NIfTI files. Default: 'grey_{simulation_name}_TI_MNI_MNI_TI_max.nii.gz' Available variables: {subject_id}, {simulation_name} dtype : numpy dtype, optional Data type to load (default: float32)

Returns:

data : ndarray NIfTI data img : nibabel Nifti1Image NIfTI image object filepath : str Full path to the loaded file

Source code in tit/core/nifti.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def load_subject_nifti_ti_toolbox(
    subject_id: str,
    simulation_name: str,
    nifti_file_pattern: str = "grey_{simulation_name}_TI_MNI_MNI_TI_max.nii.gz",
    dtype=np.float32
) -> Tuple[np.ndarray, nib.Nifti1Image, str]:
    """
    Load a NIfTI file from TI-Toolbox BIDS structure

    Parameters:
    -----------
    subject_id : str
        Subject ID (e.g., '070')
    simulation_name : str
        Simulation name (e.g., 'ICP_RHIPPO')
    nifti_file_pattern : str, optional
        Pattern for NIfTI files. Default: 'grey_{simulation_name}_TI_MNI_MNI_TI_max.nii.gz'
        Available variables: {subject_id}, {simulation_name}
    dtype : numpy dtype, optional
        Data type to load (default: float32)

    Returns:
    --------
    data : ndarray
        NIfTI data
    img : nibabel Nifti1Image
        NIfTI image object
    filepath : str
        Full path to the loaded file
    """
    pm = get_path_manager() if get_path_manager else None

    # Construct file path using TI-Toolbox path structure

    project_dir = pm.project_dir
    if not project_dir:
        raise ValueError("Project directory not found. Is PROJECT_DIR_NAME set?")

    nifti_dir = os.path.join(
        project_dir,
        const.DIR_DERIVATIVES,
        const.DIR_SIMNIBS,
        f"{const.PREFIX_SUBJECT}{subject_id}",
        "Simulations",
        simulation_name,
        "TI",
        "niftis"
    )

    # Format the filename pattern
    filename = nifti_file_pattern.format(
        subject_id=subject_id,
        simulation_name=simulation_name
    )
    filepath = os.path.join(nifti_dir, filename)

    # Load the file (inline basic loading)
    if not os.path.exists(filepath):
        # Provide extra context to make debugging path/layout issues easier
        if os.path.isdir(nifti_dir):
            try:
                existing = sorted(os.listdir(nifti_dir))
            except Exception:
                existing = []
            preview = existing[:20]
            suffix = ""
            if len(existing) > len(preview):
                suffix = f" (showing first {len(preview)} of {len(existing)})"
            raise FileNotFoundError(
                f"NIfTI file not found: {filepath}. "
                f"Directory exists: {nifti_dir}. "
                f"Files in directory: {preview}{suffix}"
            )
        raise FileNotFoundError(f"NIfTI file not found: {filepath}")

    img = nib.load(filepath)
    data = img.get_fdata(dtype=dtype)

    # Ensure 3D data (squeeze out extra dimensions if present)
    while data.ndim > 3:
        data = np.squeeze(data, axis=-1)

    return data, img, filepath

Mesh (tit.core.mesh)

Core mesh utilities for TI-toolbox.

This module contains shared mesh-related functionality used across the toolbox.

create_mesh_opt_file

create_mesh_opt_file(mesh_path, field_info=None)

Create a .opt file for Gmsh visualization of mesh fields.

Parameters:

Name Type Description Default
mesh_path str

Path to the .msh file (without .opt extension)

required
field_info dict

Dictionary with field information containing: - 'fields': list of field names to visualize - 'max_values': dict mapping field names to their max values (optional) - 'field_type': str, either 'node' or 'element' (default: 'node')

None
Source code in tit/core/mesh.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def create_mesh_opt_file(mesh_path, field_info=None):
    """
    Create a .opt file for Gmsh visualization of mesh fields.

    Parameters
    ----------
    mesh_path : str
        Path to the .msh file (without .opt extension)
    field_info : dict, optional
        Dictionary with field information containing:
        - 'fields': list of field names to visualize
        - 'max_values': dict mapping field names to their max values (optional)
        - 'field_type': str, either 'node' or 'element' (default: 'node')
    """
    if field_info is None:
        field_info = {}

    fields = field_info.get('fields', [])
    max_values = field_info.get('max_values', {})
    field_type = field_info.get('field_type', 'node')

    # Default visualization settings
    opt_content = """// Gmsh visualization settings for mesh fields
                        Mesh.SurfaceFaces = 0;       // Hide surface faces
                        Mesh.SurfaceEdges = 0;       // Hide surface edges
                        Mesh.Points = 0;             // Hide mesh points
                        Mesh.Lines = 0;              // Hide mesh lines

                        """

    # Configure each field
    for i, field_name in enumerate(fields):
        view_index = i + 1
        max_value = max_values.get(field_name, 1.0)  # Default max value

        opt_content += f"""// Make View[{view_index}] ({field_name}) visible
                            View[{view_index}].Visible = 1;
                            View[{view_index}].ColormapNumber = {i + 1};  // Use colormap {i + 1}
                            View[{view_index}].RangeType = 2;       // Custom range
                            View[{view_index}].CustomMin = 0;       // Minimum value
                            View[{view_index}].CustomMax = {max_value};  // Maximum value
                            View[{view_index}].ShowScale = 1;       // Show color scale

                            // Add alpha/transparency based on value
                            View[{view_index}].ColormapAlpha = 1;
                            View[{view_index}].ColormapAlphaPower = 0.08;

                            """

    # Add field information comments
    opt_content += "// Field information:\n"
    for i, field_name in enumerate(fields):
        max_value = max_values.get(field_name, 1.0)
        opt_content += f"// View[{i + 1}]: {field_name} field (max value: {max_value:.6f})\n"

    # Write the .opt file
    opt_path = f"{mesh_path}.opt"
    with open(opt_path, 'w') as f:
        f.write(opt_content)

    return opt_path

Calc (tit.core.calc)

Shared TI Field Calculation Utilities Used by optimization tools

get_TI_vectors

get_TI_vectors(E1_org, E2_org)

Calculate the temporal interference (TI) modulation amplitude vectors.

This function implements the Grossman et al. 2017 algorithm for computing TI vectors that represent both the direction and magnitude of maximum modulation amplitude when two sinusoidal electric fields interfere.

PHYSICAL INTERPRETATION: When two electric fields E1(t) = E1cos(2πf1t) and E2(t) = E2cos(2πf2t) with slightly different frequencies are applied simultaneously, they create a beating pattern. The TI vector indicates: - DIRECTION: Spatial direction of maximum envelope modulation - MAGNITUDE: Maximum envelope amplitude = 2 * effective_amplitude

ALGORITHM (Grossman et al. 2017): 1. Preprocessing: Ensure |E1| ≥ |E2| and acute angle α < π/2 2. Regime selection based on geometric relationship: - Regime 1 (parallel): |E2| ≤ |E1|cos(α) → TI = 2E2 - Regime 2 (oblique): |E2| > |E1|cos(α) → TI = 2E2_perpendicular_to_h where h = E1 - E2

Parameters:

Name Type Description Default
E1_org (ndarray, shape(N, 3))

Electric field vectors from electrode pair 1 [V/m]

required
E2_org (ndarray, shape(N, 3))

Electric field vectors from electrode pair 2 [V/m]

required

Returns:

Name Type Description
TI_vectors (ndarray, shape(N, 3))

TI modulation amplitude vectors [V/m] Direction: Maximum modulation direction Magnitude: Maximum envelope amplitude

References

Grossman, N. et al. (2017). Noninvasive Deep Brain Stimulation via Temporally Interfering Electric Fields. Cell, 169(6), 1029-1041.

Source code in tit/core/calc.py
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def get_TI_vectors(E1_org, E2_org):
    """
    Calculate the temporal interference (TI) modulation amplitude vectors.

    This function implements the Grossman et al. 2017 algorithm for computing
    TI vectors that represent both the direction and magnitude of maximum
    modulation amplitude when two sinusoidal electric fields interfere.

    PHYSICAL INTERPRETATION:
    When two electric fields E1(t) = E1*cos(2πf1*t) and E2(t) = E2*cos(2πf2*t)
    with slightly different frequencies are applied simultaneously, they create
    a beating pattern. The TI vector indicates:
    - DIRECTION: Spatial direction of maximum envelope modulation
    - MAGNITUDE: Maximum envelope amplitude = 2 * effective_amplitude

    ALGORITHM (Grossman et al. 2017):
    1. Preprocessing: Ensure |E1| ≥ |E2| and acute angle α < π/2
    2. Regime selection based on geometric relationship:
       - Regime 1 (parallel): |E2| ≤ |E1|cos(α) → TI = 2*E2
       - Regime 2 (oblique): |E2| > |E1|cos(α) → TI = 2*E2_perpendicular_to_h
       where h = E1 - E2

    Parameters
    ----------
    E1_org : np.ndarray, shape (N, 3)
        Electric field vectors from electrode pair 1 [V/m]
    E2_org : np.ndarray, shape (N, 3)
        Electric field vectors from electrode pair 2 [V/m]

    Returns
    -------
    TI_vectors : np.ndarray, shape (N, 3)
        TI modulation amplitude vectors [V/m]
        Direction: Maximum modulation direction
        Magnitude: Maximum envelope amplitude

    References
    ----------
    Grossman, N. et al. (2017). Noninvasive Deep Brain Stimulation via
    Temporally Interfering Electric Fields. Cell, 169(6), 1029-1041.
    """
    # Input validation
    assert E1_org.shape == E2_org.shape, "E1 and E2 must have same shape"
    assert E1_org.shape[1] == 3, "Vectors must be 3D"

    # Work with copies to avoid modifying input arrays
    E1 = E1_org.copy()
    E2 = E2_org.copy()

    # =================================================================
    # PREPROCESSING STEP 1: Magnitude ordering |E1| ≥ |E2|
    # =================================================================
    # Ensures consistency by always treating E1 as the "stronger" field
    # This simplifies the subsequent regime analysis
    idx_swap = np.linalg.norm(E2, axis=1) > np.linalg.norm(E1, axis=1)
    E1[idx_swap], E2[idx_swap] = E2[idx_swap], E1_org[idx_swap]

    # =================================================================
    # PREPROCESSING STEP 2: Acute angle constraint α < π/2
    # =================================================================
    # Ensures constructive interference by flipping E2 if dot product < 0
    # This avoids destructive interference scenarios
    idx_flip = np.sum(E1 * E2, axis=1) < 0
    E2[idx_flip] = -E2[idx_flip]

    # =================================================================
    # GEOMETRIC PARAMETERS CALCULATION
    # =================================================================
    # Calculate field magnitudes and angle between vectors
    normE1 = np.linalg.norm(E1, axis=1)
    normE2 = np.linalg.norm(E2, axis=1)

    # Safe cosine calculation to avoid division by zero and numerical errors
    denom = normE1 * normE2
    denom[denom == 0] = 1.0  # Prevent division by zero
    cosalpha = np.clip(np.sum(E1 * E2, axis=1) / denom, -1.0, 1.0)

    # =================================================================
    # REGIME SELECTION CRITERION
    # =================================================================
    # Critical condition from Grossman 2017: |E2| ≤ |E1| * cos(α)
    # This determines whether E2 is "small" relative to E1's projection
    regime1_mask = normE2 <= normE1 * cosalpha

    # Initialize output array
    TI_vectors = np.zeros_like(E1)

    # =================================================================
    # REGIME 1: PARALLEL ALIGNMENT (|E2| ≤ |E1| cos(α))
    # =================================================================
    # Physical interpretation: E2 is effectively "contained" within E1's projection
    # The TI amplitude is determined entirely by E2's magnitude and direction
    # Formula: TI = 2 * E2
    TI_vectors[regime1_mask] = 2.0 * E2[regime1_mask]

    # =================================================================
    # REGIME 2: OBLIQUE CONFIGURATION (|E2| > |E1| cos(α))
    # =================================================================
    # Physical interpretation: E2 has significant perpendicular component to E1
    # The TI is determined by the component of E2 perpendicular to h = E1 - E2
    # Formula: TI = 2 * E2_perpendicular_to_h
    regime2_mask = ~regime1_mask
    if np.any(regime2_mask):
        # Calculate difference vector h = E1 - E2
        h = E1[regime2_mask] - E2[regime2_mask]
        h_norm = np.linalg.norm(h, axis=1)

        # Handle degenerate case (h = 0) by setting unit norm
        h_norm[h_norm == 0] = 1.0
        e_h = h / h_norm[:, None]  # Unit vector along h

        # Project E2 onto h, then subtract to get perpendicular component
        # E2_perp = E2 - proj_h(E2) = E2 - (E2·ĥ)ĥ
        E2_parallel_component = np.sum(E2[regime2_mask] * e_h, axis=1)[:, None] * e_h
        E2_perp = E2[regime2_mask] - E2_parallel_component

        # The TI vector in regime 2 is twice the perpendicular component
        TI_vectors[regime2_mask] = 2.0 * E2_perp

    return TI_vectors

get_mTI_vectors

get_mTI_vectors(E1_org, E2_org, E3_org, E4_org)

Calculate multi-temporal interference (mTI) vectors from four channel E-fields.

This computes TI between channels 1 and 2 to get TI_A, TI between channels 3 and 4 to get TI_B, and finally TI between TI_A and TI_B to produce the mTI vector field.

Parameters:

Name Type Description Default
E1_org (ndarray, shape(N, 3))

Electric field vectors for channel 1

required
E2_org (ndarray, shape(N, 3))

Electric field vectors for channel 2

required
E3_org (ndarray, shape(N, 3))

Electric field vectors for channel 3

required
E4_org (ndarray, shape(N, 3))

Electric field vectors for channel 4

required

Returns:

Name Type Description
mTI_vectors (ndarray, shape(N, 3))

Multi-TI modulation amplitude vectors

Source code in tit/core/calc.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def get_mTI_vectors(E1_org, E2_org, E3_org, E4_org):
    """
    Calculate multi-temporal interference (mTI) vectors from four channel E-fields.

    This computes TI between channels 1 and 2 to get TI_A, TI between channels 3 and 4
    to get TI_B, and finally TI between TI_A and TI_B to produce the mTI vector field.

    Parameters
    ----------
    E1_org : np.ndarray, shape (N, 3)
        Electric field vectors for channel 1
    E2_org : np.ndarray, shape (N, 3)
        Electric field vectors for channel 2
    E3_org : np.ndarray, shape (N, 3)
        Electric field vectors for channel 3
    E4_org : np.ndarray, shape (N, 3)
        Electric field vectors for channel 4

    Returns
    -------
    mTI_vectors : np.ndarray, shape (N, 3)
        Multi-TI modulation amplitude vectors
    """
    # Validate shapes
    for i, arr in enumerate([E1_org, E2_org, E3_org, E4_org], start=1):
        if arr.ndim != 2 or arr.shape[1] != 3:
            raise ValueError(f"E{i}_org must have shape (N, 3), got {arr.shape}")

    if not (E1_org.shape == E2_org.shape == E3_org.shape == E4_org.shape):
        raise ValueError(
            "All input arrays must have identical shapes. "
            f"Got: {[E1_org.shape, E2_org.shape, E3_org.shape, E4_org.shape]}"
        )

    # Step 1: TI between (E1, E2)
    TI_A = get_TI_vectors(E1_org, E2_org)

    # Step 2: TI between (E3, E4)
    TI_B = get_TI_vectors(E3_org, E4_org)

    # Step 3: TI between (TI_A, TI_B) → mTI
    mTI_vectors = get_TI_vectors(TI_A, TI_B)

    return mTI_vectors