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 ANALYZER fill:#2d5a27,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(
    project_dir="/data/my_project",
    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.

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: Subject identifier (without sub- prefix). simulation: Simulation (montage) folder name. space: "mesh" or "voxel". output_dir: Override output directory. If None, derived from PathManager.

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: (x, y, z) coordinates of the sphere centre. radius: Radius in mm. coordinate_space: "subject" (default) or "MNI". visualize: Generate overlay, histogram and CSV artifacts.

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:
        (x, y, z) coordinates of the sphere centre.
    radius:
        Radius in mm.
    coordinate_space:
        ``"subject"`` (default) or ``"MNI"``.
    visualize:
        Generate overlay, histogram and CSV artifacts.
    """
    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: Atlas name recognised by SimNIBS (e.g. "DK40", "HCP_MMP1"). region: Region name within the atlas, or a list of region names whose masks will be unioned into a single combined ROI. visualize: Generate overlay, histogram and CSV artifacts.

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:
        Atlas name recognised by SimNIBS (e.g. ``"DK40"``, ``"HCP_MMP1"``).
    region:
        Region name within the atlas, or a list of region names whose
        masks will be unioned into a single combined ROI.
    visualize:
        Generate overlay, histogram and CSV artifacts.
    """
    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.

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 subject_ids 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. Returns a :class:GroupResult.

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 *subject_ids* 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.  Returns a :class:`GroupResult`.
    """
    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,
    )