Skip to content

analyzer

tit.analyzer

Unified field analysis for mesh and voxel spaces.

Provides single-subject ROI analysis (spherical and cortical), multi-subject group analysis with summary statistics, and automatic field file selection for TI and mTI simulations.

Public API

Analyzer Single-subject field analyzer for mesh and voxel spaces. AnalysisResult Typed container for per-subject ROI statistics. GroupResult Container for multi-subject group analysis outcomes. run_group_analysis Run the same ROI analysis across multiple subjects and summarise. select_field_file Resolve the correct field file path for a given subject/simulation/space.

See Also

tit.stats : Cluster-based permutation testing for group-level inference. tit.sim : TI/mTI simulation engine that produces the field files analyzed here.

Analyzer

Analyzer(subject_id: str, simulation: str, space: str = 'mesh', tissue_type: str = 'GM', output_dir: str | None = None)

Unified analyzer for mesh and voxel field data.

Lazily loads the field file on first analysis call. All coordinate transforms and ROI masking are handled internally.

Parameters

subject_id : str Subject identifier (without sub- prefix). simulation : str Simulation (montage) folder name. space : str, optional "mesh" or "voxel". Default "mesh". tissue_type : str, optional "GM", "WM", or "both". Only affects voxel analyses; mesh analyses always use the GM cortical surface. Default "GM". output_dir : str or None, optional Override output directory. If None, derived from PathManager.

Attributes

subject_id : str Subject identifier. simulation : str Simulation folder name. space : str Analysis space ("mesh" or "voxel"). tissue_type : str Normalised tissue selection ("GM", "WM", or "BOTH"). field_path : pathlib.Path Resolved path to the field file. field_name : str Short name of the field (e.g. "TI_max"). m2m_path : str Path to the subject's m2m_* directory. output_dir : str or None Output directory override, or None.

Examples

from tit.analyzer import Analyzer analyzer = Analyzer("001", "montage_bipolar", space="mesh") result = analyzer.analyze_sphere( ... center=(-30.0, -20.0, 50.0), radius=10.0, ... ) print(result.roi_mean, result.roi_focality)

See Also

AnalysisResult : Container for single-subject analysis outputs. run_group_analysis : Multi-subject group analysis.

Source code in tit/analyzer/analyzer.py
def __init__(
    self,
    subject_id: str,
    simulation: str,
    space: str = "mesh",
    tissue_type: str = "GM",
    output_dir: str | None = None,
) -> None:
    self.subject_id = subject_id
    self.simulation = simulation
    self.space = space
    self.tissue_type = self._normalize_tissue_type(tissue_type)
    if self.space == "mesh":
        self.tissue_type = "GM"

    field_path, field_name = select_field_file(
        subject_id,
        simulation,
        space,
        tissue_type=self.tissue_type,
    )
    self.field_path = field_path
    self.field_name = field_name

    pm = get_path_manager()
    self.m2m_path = pm.m2m(subject_id)
    self.output_dir = output_dir
    self._pm = pm

    # Attach a file handler so every log message is persisted to disk
    logs_dir = pm.logs(subject_id)
    pm.ensure(logs_dir)
    timestamp = time.strftime("%Y%m%d_%H%M%S")
    log_file = Path(logs_dir) / f"analyzer_{simulation}_{timestamp}.log"
    self._log_handler = add_file_handler(log_file)

    logger.info(
        "Analyzer initialised: subject=%s sim=%s space=%s tissue=%s",
        subject_id,
        simulation,
        space,
        self.tissue_type,
    )

    # Cached lazily
    self._surface_mesh = None
    self._surface_mesh_path: Path | None = None

analyze_sphere

analyze_sphere(center: tuple[float, float, float], radius: float, coordinate_space: str = 'subject', visualize: bool = False) -> AnalysisResult

Analyze a spherical ROI.

Parameters

center : tuple of float (x, y, z) coordinates of the sphere centre. radius : float Radius in mm. coordinate_space : str, optional "subject" (default) or "MNI". When "MNI", coordinates are transformed to subject space via SimNIBS mni2subject_coords. visualize : bool, optional Generate overlay, histogram, and CSV artifacts.

Returns

AnalysisResult ROI and whole-GM statistics for the spherical region.

Raises

FileNotFoundError If the required field or surface mesh file does not exist.

See Also

analyze_cortex : Atlas-based cortical ROI analysis.

Source code in tit/analyzer/analyzer.py
def analyze_sphere(
    self,
    center: tuple[float, float, float],
    radius: float,
    coordinate_space: str = "subject",
    visualize: bool = False,
) -> AnalysisResult:
    """Analyze a spherical ROI.

    Parameters
    ----------
    center : tuple of float
        ``(x, y, z)`` coordinates of the sphere centre.
    radius : float
        Radius in mm.
    coordinate_space : str, optional
        ``"subject"`` (default) or ``"MNI"``. When ``"MNI"``,
        coordinates are transformed to subject space via SimNIBS
        ``mni2subject_coords``.
    visualize : bool, optional
        Generate overlay, histogram, and CSV artifacts.

    Returns
    -------
    AnalysisResult
        ROI and whole-GM statistics for the spherical region.

    Raises
    ------
    FileNotFoundError
        If the required field or surface mesh file does not exist.

    See Also
    --------
    analyze_cortex : Atlas-based cortical ROI analysis.
    """
    from tit.telemetry import track_operation
    from tit import constants as const

    with track_operation(const.TELEMETRY_OP_ANALYSIS):
        dispatch = {"mesh": self._sphere_mesh, "voxel": self._sphere_voxel}
        return dispatch[self.space](center, radius, coordinate_space, visualize)

analyze_cortex

analyze_cortex(atlas: str, region: str | list[str], visualize: bool = False) -> AnalysisResult

Analyze a cortical atlas region.

Parameters

atlas : str Atlas name recognised by SimNIBS (e.g. "DK40", "HCP_MMP1"), or an absolute path to an atlas NIfTI (voxel mode only). region : str or list of str Region name within the atlas (e.g. "lh.cuneus"), or a list of region names whose masks are unioned into a single combined ROI. Bare names like "cuneus" expand to both hemispheres in mesh mode. visualize : bool, optional Generate overlay, histogram, and CSV artifacts.

Returns

AnalysisResult ROI and whole-GM statistics for the cortical region.

Raises

KeyError If a region name cannot be resolved in the atlas. FileNotFoundError If the atlas or field file does not exist.

See Also

analyze_sphere : Spherical ROI analysis.

Source code in tit/analyzer/analyzer.py
def analyze_cortex(
    self,
    atlas: str,
    region: str | list[str],
    visualize: bool = False,
) -> AnalysisResult:
    """Analyze a cortical atlas region.

    Parameters
    ----------
    atlas : str
        Atlas name recognised by SimNIBS (e.g. ``"DK40"``,
        ``"HCP_MMP1"``), or an absolute path to an atlas NIfTI
        (voxel mode only).
    region : str or list of str
        Region name within the atlas (e.g. ``"lh.cuneus"``), or a
        list of region names whose masks are unioned into a single
        combined ROI. Bare names like ``"cuneus"`` expand to both
        hemispheres in mesh mode.
    visualize : bool, optional
        Generate overlay, histogram, and CSV artifacts.

    Returns
    -------
    AnalysisResult
        ROI and whole-GM statistics for the cortical region.

    Raises
    ------
    KeyError
        If a region name cannot be resolved in the atlas.
    FileNotFoundError
        If the atlas or field file does not exist.

    See Also
    --------
    analyze_sphere : Spherical ROI analysis.
    """
    from tit.telemetry import track_operation
    from tit import constants as const

    with track_operation(const.TELEMETRY_OP_ANALYSIS):
        dispatch = {"mesh": self._cortex_mesh, "voxel": self._cortex_voxel}
        return dispatch[self.space](atlas, region, visualize)

AnalysisResult dataclass

AnalysisResult(field_name: str, region_name: str, space: str, analysis_type: str, roi_mean: float, roi_max: float, roi_min: float, roi_focality: float, gm_mean: float, gm_max: float, normal_mean: float | None = None, normal_max: float | None = None, normal_focality: float | None = None, percentile_95: float | None = None, percentile_99: float | None = None, percentile_99_9: float | None = None, focality_50_area: float | None = None, focality_75_area: float | None = None, focality_90_area: float | None = None, focality_95_area: float | None = None, n_elements: int = 0, total_area_or_volume: float = 0.0)

Immutable container for ROI analysis statistics.

Returned by :meth:Analyzer.analyze_sphere and :meth:Analyzer.analyze_cortex.

Attributes

field_name : str Name of the field that was analyzed (e.g. "TI_max"). region_name : str Human-readable ROI label. space : str "mesh" or "voxel". analysis_type : str "spherical" or "cortical". roi_mean : float Area/volume-weighted mean field value inside the ROI. roi_max : float Maximum field value inside the ROI. roi_min : float Minimum field value inside the ROI. roi_focality : float Ratio of ROI mean to whole-GM mean (> 1 means the ROI is stronger than the GM average). gm_mean : float Mean field value across all positive GM elements. gm_max : float Maximum field value across all GM elements. normal_mean : float or None Weighted mean of the normal-component field in the ROI (mesh only; None when unavailable). normal_max : float or None Maximum normal-component field in the ROI. normal_focality : float or None Normal-component focality ratio. percentile_95 : float or None 95th percentile of the whole-GM field distribution. percentile_99 : float or None 99th percentile of the whole-GM field distribution. percentile_99_9 : float or None 99.9th percentile of the whole-GM field distribution. focality_50_area : float or None Area/volume (cm2/cm3) where the field exceeds 50 % of the 99.9th percentile value. focality_75_area : float or None Same for 75 % threshold. focality_90_area : float or None Same for 90 % threshold. focality_95_area : float or None Same for 95 % threshold. n_elements : int Number of mesh nodes or voxels in the ROI mask. total_area_or_volume : float Total surface area (mm^2) or volume (mm^3) of positive-valued ROI elements.

See Also

Analyzer : Single-subject field analyzer. GroupResult : Container for multi-subject group analysis.

GroupResult dataclass

GroupResult(subject_results: dict[str, AnalysisResult], summary_csv_path: Path, comparison_plot_path: Path | None)

Outcome of a multi-subject group analysis.

Attributes

subject_results : dict of str to AnalysisResult Mapping of subject ID to its :class:~tit.analyzer.analyzer.AnalysisResult. summary_csv_path : pathlib.Path Path to the summary CSV (one row per subject plus an AVERAGE row). comparison_plot_path : pathlib.Path or None Path to the 2x2 comparison bar-chart PDF, or None if plotting failed.

See Also

run_group_analysis : Factory function that produces this result. AnalysisResult : Per-subject analysis container.

select_field_file

select_field_file(subject_id: str, simulation: str, space: str, tissue_type: str = 'GM') -> tuple[Path, str]

Return the field file path and SimNIBS field name.

Detects whether the simulation is TI (2-pair) or mTI (4-pair) by checking for the existence of the mTI mesh directory.

Parameters

subject_id : str Subject identifier (without sub- prefix). simulation : str Simulation (montage) folder name. space : str "mesh" or "voxel". tissue_type : str, optional "GM", "WM", or "both" (voxel only). Default "GM".

Returns

field_path : pathlib.Path Resolved absolute path to the field file. field_name : str SimNIBS field name (e.g. "TI_max", "mTI_max").

Raises

FileNotFoundError If the expected field file does not exist. ValueError If space is not "mesh" or "voxel".

See Also

Analyzer : Consumes the resolved path to load and analyze fields.

Source code in tit/analyzer/field_selector.py
def select_field_file(
    subject_id: str,
    simulation: str,
    space: str,
    tissue_type: str = "GM",
) -> tuple[Path, str]:
    """Return the field file path and SimNIBS field name.

    Detects whether the simulation is TI (2-pair) or mTI (4-pair) by checking
    for the existence of the mTI mesh directory.

    Parameters
    ----------
    subject_id : str
        Subject identifier (without ``sub-`` prefix).
    simulation : str
        Simulation (montage) folder name.
    space : str
        ``"mesh"`` or ``"voxel"``.
    tissue_type : str, optional
        ``"GM"``, ``"WM"``, or ``"both"`` (voxel only). Default ``"GM"``.

    Returns
    -------
    field_path : pathlib.Path
        Resolved absolute path to the field file.
    field_name : str
        SimNIBS field name (e.g. ``"TI_max"``, ``"mTI_max"``).

    Raises
    ------
    FileNotFoundError
        If the expected field file does not exist.
    ValueError
        If *space* is not ``"mesh"`` or ``"voxel"``.

    See Also
    --------
    Analyzer : Consumes the resolved path to load and analyze fields.
    """
    pm = get_path_manager()
    sim_dir = Path(pm.simulation(subject_id, simulation))
    is_mti = (sim_dir / "mTI" / "mesh").is_dir()

    if space == "mesh":
        return _select_mesh(sim_dir, simulation, is_mti)
    if space == "voxel":
        return _select_voxel(sim_dir, is_mti, tissue_type)
    raise ValueError(f"Unsupported space: {space!r} (expected 'mesh' or 'voxel')")

run_group_analysis

run_group_analysis(subject_ids: list[str], simulation: str, space: str = 'mesh', tissue_type: str = 'GM', analysis_type: str = 'spherical', center: tuple[float, float, float] | None = None, radius: float | None = None, coordinate_space: str = 'subject', atlas: str | None = None, region: str | list[str] | None = None, visualize: bool = False, output_dir: str | Path | None = None) -> GroupResult

Run the same ROI analysis across multiple subjects and summarise.

Dispatches to analyze_sphere or analyze_cortex on each subject, builds a summary CSV (with an AVERAGE row), and generates a 2x2 comparison bar-chart PDF.

Parameters

subject_ids : list of str Subject identifiers (without sub- prefix). simulation : str Simulation (montage) folder name, shared by all subjects. space : str, optional "mesh" or "voxel". Default "mesh". tissue_type : str, optional "GM", "WM", or "both" (voxel only). Default "GM". analysis_type : str, optional "spherical" or "cortical". Default "spherical". center : tuple of float or None, optional (x, y, z) sphere centre; required when analysis_type is "spherical". radius : float or None, optional Sphere radius in mm; required when analysis_type is "spherical". coordinate_space : str, optional "subject" or "MNI" (spherical only). Default "subject". atlas : str or None, optional Atlas name (cortical only). region : str, list of str, or None, optional Region name or list of region names (cortical only). visualize : bool, optional Generate per-subject visualization artifacts. Default False. output_dir : str, pathlib.Path, or None, optional Override output directory. If None, derived from PathManager.

Returns

GroupResult Per-subject results, the summary CSV path, and the comparison plot path.

Raises

KeyError If analysis_type is not "spherical" or "cortical".

See Also

Analyzer : Single-subject analyzer used internally per subject. GroupResult : Container for the returned outcomes.

Source code in tit/analyzer/group.py
def run_group_analysis(
    subject_ids: list[str],
    simulation: str,
    space: str = "mesh",
    tissue_type: str = "GM",
    analysis_type: str = "spherical",
    center: tuple[float, float, float] | None = None,
    radius: float | None = None,
    coordinate_space: str = "subject",
    atlas: str | None = None,
    region: str | list[str] | None = None,
    visualize: bool = False,
    output_dir: str | Path | None = None,
) -> GroupResult:
    """Run the same ROI analysis across multiple subjects and summarise.

    Dispatches to ``analyze_sphere`` or ``analyze_cortex`` on each subject,
    builds a summary CSV (with an AVERAGE row), and generates a 2x2
    comparison bar-chart PDF.

    Parameters
    ----------
    subject_ids : list of str
        Subject identifiers (without ``sub-`` prefix).
    simulation : str
        Simulation (montage) folder name, shared by all subjects.
    space : str, optional
        ``"mesh"`` or ``"voxel"``. Default ``"mesh"``.
    tissue_type : str, optional
        ``"GM"``, ``"WM"``, or ``"both"`` (voxel only). Default ``"GM"``.
    analysis_type : str, optional
        ``"spherical"`` or ``"cortical"``. Default ``"spherical"``.
    center : tuple of float or None, optional
        ``(x, y, z)`` sphere centre; required when *analysis_type* is
        ``"spherical"``.
    radius : float or None, optional
        Sphere radius in mm; required when *analysis_type* is
        ``"spherical"``.
    coordinate_space : str, optional
        ``"subject"`` or ``"MNI"`` (spherical only). Default ``"subject"``.
    atlas : str or None, optional
        Atlas name (cortical only).
    region : str, list of str, or None, optional
        Region name or list of region names (cortical only).
    visualize : bool, optional
        Generate per-subject visualization artifacts. Default ``False``.
    output_dir : str, pathlib.Path, or None, optional
        Override output directory. If ``None``, derived from PathManager.

    Returns
    -------
    GroupResult
        Per-subject results, the summary CSV path, and the comparison
        plot path.

    Raises
    ------
    KeyError
        If *analysis_type* is not ``"spherical"`` or ``"cortical"``.

    See Also
    --------
    Analyzer : Single-subject analyzer used internally per subject.
    GroupResult : Container for the returned outcomes.
    """
    from tit.telemetry import track_operation
    from tit import constants as _const

    with track_operation(_const.TELEMETRY_OP_GROUP_ANALYSIS):
        out = _resolve_output_dir(output_dir)

        # File handler so group analysis logs are persisted
        timestamp = time.strftime("%Y%m%d_%H%M%S")
        log_file = out / f"group_analysis_{timestamp}.log"
        add_file_handler(log_file)

        logger.info(
            "Group analysis started: %d subjects, space=%s, type=%s",
            len(subject_ids),
            space,
            analysis_type,
        )

        dispatch: dict[str, callable] = {
            "spherical": lambda a: a.analyze_sphere(
                center=center,
                radius=radius,
                coordinate_space=coordinate_space,
                visualize=visualize,
            ),
            "cortical": lambda a: a.analyze_cortex(
                atlas=atlas,
                region=region,
                visualize=visualize,
            ),
        }
        analyze_fn = dispatch[analysis_type]

        results: dict[str, AnalysisResult] = {}
        for sid in subject_ids:
            logger.info("Analyzing subject %s", sid)
            results[sid] = analyze_fn(Analyzer(sid, simulation, space, tissue_type))

        df = _build_summary_df(results)
        csv_path = out / "group_summary.csv"
        df.to_csv(csv_path, index=False, float_format="%.3f")
        logger.info("Summary CSV written to %s", csv_path)

        plot_path = _generate_comparison_plot(df, out)
        logger.info("Comparison plot written to %s", plot_path)

        return GroupResult(
            subject_results=results,
            summary_csv_path=csv_path,
            comparison_plot_path=plot_path,
        )