Skip to content

Analysis

The analysis module evaluates simulation results by computing statistics within regions of interest (ROIs). It supports spherical and cortical atlas ROIs in both mesh and voxel space.

graph LR
    FIELD([Field Data]) --> ANALYZER[Analyzer]
    ROI([ROI Specification]) --> ANALYZER
    ANALYZER --> STATS([ROI Statistics])
    ANALYZER --> VIZ([Visualizations])
    ANALYZER --> CSV([CSV Export])
    style FIELD fill:#1a3a5c,stroke:#48a,color:#fff
    style ROI fill:#1a3a5c,stroke:#48a,color:#fff
    style ANALYZER fill:#2d5a27,stroke:#4a8,color:#fff
    style STATS fill:#1a5c4a,stroke:#4a8,color:#fff
    style VIZ fill:#1a5c4a,stroke:#4a8,color:#fff
    style CSV fill:#1a5c4a,stroke:#4a8,color:#fff

Single-Subject Analysis

Spherical ROI

from tit.analyzer import Analyzer

analyzer = Analyzer(
    subject_id="001",
    simulation="motor_cortex",
    space="mesh",
    tissue_type="GM",  # "GM", "WM", or "both" (voxel only; mesh always uses GM)
)
result = analyzer.analyze_sphere(
    center=(-42, -20, 55),
    radius=10,
    coordinate_space="MNI",
    visualize=True,
)

# Access metrics
print(f"ROI Mean:     {result.roi_mean:.4f} V/m")
print(f"ROI Max:      {result.roi_max:.4f} V/m")
print(f"ROI Min:      {result.roi_min:.4f} V/m")
print(f"Focality:     {result.roi_focality:.2f}")
print(f"GM Mean:      {result.gm_mean:.4f} V/m")
print(f"GM Max:       {result.gm_max:.4f} V/m")
print(f"N elements:   {result.n_elements}")

Cortical Atlas ROI

The region parameter accepts a single region name or a list of region names whose masks are combined into one ROI.

result = analyzer.analyze_cortex(atlas="DK40", region="precentral-lh")

# Multiple regions combined into a single ROI
result = analyzer.analyze_cortex(atlas="DK40", region=["precentral-lh", "postcentral-lh"])

Analysis Spaces

Space Method Description
"mesh" Surface-based Analysis on the cortical mesh, weighted by node areas
"voxel" Volume-based Analysis on NIfTI data, weighted by voxel volumes

When to Use Each Space

Use mesh space for cortical targets where surface geometry matters (e.g., normal/tangential field components). Use voxel space for deep brain targets or when you need MNI-aligned volumetric analysis.

ROI Types

Define a sphere by center coordinates and radius. Works in both MNI and subject coordinate spaces.

result = analyzer.analyze_sphere(
    center=(-42, -20, 55),
    radius=10,
    coordinate_space="MNI",  # or "subject" (default)
)

Use a predefined cortical atlas parcellation (e.g., Desikan-Killiany DK40, HCP_MMP1). A single region or a list of regions can be provided.

result = analyzer.analyze_cortex(
    atlas="DK40",
    region="precentral-lh",
)

# Or combine multiple regions into one ROI
result = analyzer.analyze_cortex(
    atlas="HCP_MMP1",
    region=["4-lh", "3a-lh", "3b-lh"],
)

Result Metrics

Each analysis returns an AnalysisResult dataclass with these fields:

Field Description
field_name SimNIBS field name (e.g., "TI_max")
region_name Human-readable ROI identifier
space "mesh" or "voxel"
analysis_type "spherical" or "cortical"
roi_mean Weighted mean field intensity within the ROI
roi_max Maximum field intensity within the ROI
roi_min Minimum field intensity within the ROI
roi_focality Ratio of ROI mean intensity to whole-tissue mean
gm_mean Mean field intensity across all gray matter (or tissue)
gm_max Maximum field intensity across all gray matter (or tissue)
normal_mean Mean of the normal-component field in the ROI (mesh only, optional)
normal_max Max of the normal-component field in the ROI (mesh only, optional)
normal_focality Focality of the normal-component field (mesh only, optional)
percentile_95 95th percentile of field intensity (area/volume-weighted)
percentile_99 99th percentile of field intensity
percentile_99_9 99.9th percentile of field intensity
focality_50_area Area/volume (cm^2) above 50% of 99.9th percentile
focality_75_area Area/volume (cm^2) above 75% of 99.9th percentile
focality_90_area Area/volume (cm^2) above 90% of 99.9th percentile
focality_95_area Area/volume (cm^2) above 95% of 99.9th percentile
n_elements Number of mesh nodes or voxels in the ROI
total_area_or_volume Total area (mm^2, mesh) or volume (mm^3, voxel) of the ROI

Group Analysis

Compare results across multiple subjects:

from tit.analyzer import run_group_analysis

group_result = run_group_analysis(
    subject_ids=["001", "002", "003"],
    simulation="motor_cortex",
    space="mesh",
    tissue_type="GM",
    analysis_type="spherical",
    center=(-42, -20, 55),
    radius=10,
    coordinate_space="MNI",
    visualize=True,
)

# group_result.subject_results: dict of per-subject AnalysisResult
# group_result.summary_csv_path: path to group_summary.csv
# group_result.comparison_plot_path: path to comparison bar chart PDF

The run_group_analysis function also supports cortical atlas ROIs:

group_result = run_group_analysis(
    subject_ids=["001", "002", "003"],
    simulation="motor_cortex",
    space="mesh",
    analysis_type="cortical",
    atlas="DK40",
    region="precentral-lh",
    visualize=True,
)

Statistical Testing

For formal group comparisons (e.g., responders vs non-responders), use the tit.stats module:

from tit.stats import GroupComparisonConfig, run_group_comparison

# Load subjects from CSV (columns: subject_id, simulation_name, response)
subjects = GroupComparisonConfig.load_subjects("/data/my_project/subjects.csv")

config = GroupComparisonConfig(
    analysis_name="responder_comparison",
    subjects=subjects,
    test_type=GroupComparisonConfig.TestType.UNPAIRED,
    n_permutations=5000,
    alpha=0.05,
    cluster_threshold=0.05,
)

result = run_group_comparison(config)
print(f"Significant clusters: {result.n_significant_clusters}")
print(f"Significant voxels:   {result.n_significant_voxels}")

The tit.stats module also supports voxel-wise correlation analysis via CorrelationConfig and run_correlation.

API Reference

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

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

tit.analyzer.group.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.

tit.analyzer.group.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,
        )