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 (cm^2, mesh) or volume (cm^3, voxel) above 50% of 99.9th percentile
focality_75_area Area (cm^2, mesh) or volume (cm^3, voxel) above 75% of 99.9th percentile
focality_90_area Area (cm^2, mesh) or volume (cm^3, voxel) above 90% of 99.9th percentile
focality_95_area Area (cm^2, mesh) or volume (cm^3, voxel) 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", "cortical" or "mask". 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 where the field exceeds 50 % of the 99.9th percentile value -- cm^2 in mesh space, cm^3 in voxel space (the _area name is kept for both spaces). 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, field: 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. field : str or None, optional Field to analyze, from constants.FIELD_REGISTRY (e.g. "hf_peak", "TI_normal"). Default None resolves the TI_max/mTI_max envelope. See :func:select_field_file.

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.

Raises

FileNotFoundError If no field file exists for subject_id/simulation in space (run the simulation first). ValueError If space is not "mesh"/"voxel" or field is not a known field name.

Examples

from tit.analyzer import Analyzer analyzer = Analyzer("ernie", "L_Insula", space="voxel") # doctest: +SKIP result = analyzer.analyze_sphere( ... center=(-35.0, 5.0, 5.0), radius=10.0, coordinate_space="MNI", ... ) # doctest: +SKIP result.roi_mean, result.roi_focality # doctest: +SKIP (0.21, 1.8) cortex = Analyzer("ernie", "L_Insula", space="mesh").analyze_cortex( ... atlas="DK40", region="lh.insula", visualize=True) # doctest: +SKIP cortex.analysis_type, cortex.focality_50_area # doctest: +SKIP ('cortical', 42.1)

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,
    field: 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,
        field=field,
    )
    self.field_path = field_path
    self.field_name = field_name
    # mTI outputs live under <simulation>/mTI/; TI outputs under <simulation>/TI/.
    self._is_mti = "mTI" in Path(field_path).parts

    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 field=%s",
        subject_id,
        simulation,
        space,
        self.tissue_type,
        field_name,
    )

    # 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 Write the ROI overlay, its scene, histogram.png and analysis.json.

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
        Write the ROI overlay, its scene, ``histogram.png`` and ``analysis.json``.

    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[0], center[1], center[2], radius)],
            coordinate_space,
            visualize,
        )

analyze_spheres

analyze_spheres(spheres, coordinate_space: str = 'subject', visualize: bool = False) -> AnalysisResult

Analyze several spherical ROIs unioned into a single ROI.

The spheres are combined with a logical OR before any statistic is computed, so the result describes one ROI covering all of them -- overlapping spheres are not double-counted. Passing a single sphere is equivalent to :meth:analyze_sphere.

Parameters

spheres : sequence of tuple of float One (x, y, z, r) per sphere. coordinate_space : str, optional "subject" (default) or "MNI", applied to every sphere. visualize : bool, optional Write the ROI overlay, its scene, histogram.png and analysis.json.

Returns

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

Raises

ValueError If spheres is empty.

See Also

analyze_sphere : Single spherical ROI analysis.

Source code in tit/analyzer/analyzer.py
def analyze_spheres(
    self,
    spheres,
    coordinate_space: str = "subject",
    visualize: bool = False,
) -> AnalysisResult:
    """Analyze several spherical ROIs unioned into a single ROI.

    The spheres are combined with a logical OR before any statistic is
    computed, so the result describes one ROI covering all of them --
    overlapping spheres are not double-counted. Passing a single sphere
    is equivalent to :meth:`analyze_sphere`.

    Parameters
    ----------
    spheres : sequence of tuple of float
        One ``(x, y, z, r)`` per sphere.
    coordinate_space : str, optional
        ``"subject"`` (default) or ``"MNI"``, applied to every sphere.
    visualize : bool, optional
        Write the ROI overlay, its scene, ``histogram.png`` and ``analysis.json``.

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

    Raises
    ------
    ValueError
        If *spheres* is empty.

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

    spheres = [tuple(float(v) for v in s) for s in spheres]
    if not spheres:
        raise ValueError("analyze_spheres requires at least one sphere.")

    with track_operation(const.TELEMETRY_OP_ANALYSIS):
        dispatch = {"mesh": self._sphere_mesh, "voxel": self._sphere_voxel}
        return dispatch[self.space](spheres, 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 Write the ROI overlay, its scene, histogram.png and analysis.json.

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
        Write the ROI overlay, its scene, ``histogram.png`` and ``analysis.json``.

    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)

analyze_mask

analyze_mask(mask_path: str, coordinate_space: str = 'subject', visualize: bool = False) -> AnalysisResult

Analyze positive NIfTI voxels sampled onto subject geometry.

MNI masks use the optimizer's nonlinear m2m registration. Nearest-neighbour sampling preserves binary membership; mesh results remain GM surface-area statistics and voxel results retain the selected tissue and volume units.

Parameters

mask_path : str Path to a 3-D NIfTI mask (.nii/.nii.gz); voxels > 0 form the ROI. coordinate_space : str, optional "subject" (default) or "mni" (case-insensitive). visualize : bool, optional Write the ROI overlay, its scene, histogram.png and analysis.json.

Returns

AnalysisResult ROI and whole-GM statistics for the mask region, with analysis_type="mask".

Raises

ValueError If coordinate_space is not "subject"/"mni", the file is not a readable finite 3-D NIfTI, or (mesh mode) the mask does not overlap the grey-matter surface.

See Also

analyze_sphere : Spherical ROI analysis. analyze_cortex : Atlas-based cortical ROI analysis.

Source code in tit/analyzer/analyzer.py
def analyze_mask(
    self,
    mask_path: str,
    coordinate_space: str = "subject",
    visualize: bool = False,
) -> AnalysisResult:
    """Analyze positive NIfTI voxels sampled onto subject geometry.

    MNI masks use the optimizer's nonlinear m2m registration. Nearest-neighbour
    sampling preserves binary membership; mesh results remain GM surface-area
    statistics and voxel results retain the selected tissue and volume units.

    Parameters
    ----------
    mask_path : str
        Path to a 3-D NIfTI mask (``.nii``/``.nii.gz``); voxels ``> 0``
        form the ROI.
    coordinate_space : str, optional
        ``"subject"`` (default) or ``"mni"`` (case-insensitive).
    visualize : bool, optional
        Write the ROI overlay, its scene, ``histogram.png`` and ``analysis.json``.

    Returns
    -------
    AnalysisResult
        ROI and whole-GM statistics for the mask region, with
        ``analysis_type="mask"``.

    Raises
    ------
    ValueError
        If *coordinate_space* is not ``"subject"``/``"mni"``, the file is
        not a readable finite 3-D NIfTI, or (mesh mode) the mask does not
        overlap the grey-matter surface.

    See Also
    --------
    analyze_sphere : Spherical ROI analysis.
    analyze_cortex : Atlas-based cortical ROI analysis.
    """
    import tempfile
    import nibabel as nib
    from nibabel.processing import resample_from_to
    from scipy.ndimage import map_coordinates
    from tit.opt.masks import prepare_mask
    from tit.analyzer.masks import mask_region_name
    from tit import constants as const
    from tit.telemetry import track_operation

    coordinate_space = coordinate_space.lower()
    region_name = mask_region_name(mask_path, coordinate_space)
    with (
        track_operation(const.TELEMETRY_OP_ANALYSIS),
        tempfile.TemporaryDirectory() as scratch,
    ):
        prepared = prepare_mask(
            mask_path, coordinate_space, str(self.m2m_path), scratch, binary=True
        )
        mask_img = nib.load(prepared)
        if self.space == "mesh":
            surface = self._load_surface_mesh()
            coords = nib.affines.apply_affine(
                np.linalg.inv(mask_img.affine), surface.nodes.node_coord
            )
            mask = (
                map_coordinates(
                    mask_img.get_fdata(), coords.T, order=0, mode="constant", cval=0
                )
                > 0
            )
            if not mask.any():
                raise ValueError("Mask does not overlap the gray-matter surface")
            result = self._analyze_mesh_roi(
                surface,
                self._field_values(surface),
                self._node_areas(surface),
                mask,
                region_name=region_name,
                analysis_type="mask",
                visualize=visualize,
            )
            return result
        img = nib.load(str(self.field_path))
        field_arr = self._squeeze_4d(img.get_fdata())
        sampled = (
            resample_from_to(
                mask_img,
                (field_arr.shape[:3], img.affine),
                order=0,
                mode="constant",
                cval=0,
            ).get_fdata()
            > 0
        )
        analysis_mask = (field_arr > 0) & self._voxel_tissue_mask(
            img, field_arr.shape[:3], img.affine
        )
        roi_mask = sampled & analysis_mask
        if not roi_mask.any():
            raise ValueError(
                "Mask does not overlap positive field values in the selected tissue"
            )
        result = self._analyze_voxel_roi(
            field_arr,
            roi_mask,
            analysis_mask,
            img.affine,
            region_name=region_name,
            analysis_type="mask",
            visualize=visualize,
        )
        return result

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', spheres: Sequence[tuple[float, float, float, float]] | None = None, atlas: str | None = None, region: str | list[str] | None = None, visualize: bool = False, output_dir: str | Path | None = None, field: str | None = None, mask_path: str | 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", "cortical" or "mask". 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 and mask). Default "subject"; a group mask analysis requires "MNI". spheres : sequence of tuple of float or None, optional One (x, y, z, r) per sphere, unioned into a single ROI (spherical only). Takes precedence over center/radius. 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. field : str or None, optional Field to analyze (constants.FIELD_REGISTRY name, e.g. "TI_max", "TI_normal", "hf_peak"). Default None resolves the TI_max/mTI_max envelope per subject. mask_path : str or None, optional MNI-space NIfTI mask (analysis_type="mask" only).

Returns

GroupResult Per-subject results, the summary CSV path (group_summary.csv), and the comparison plot path.

Raises

KeyError If analysis_type is not "spherical", "cortical" or "mask". ValueError If analysis_type="mask" with a non-MNI coordinate_space or an unreadable mask_path. FileNotFoundError If a subject has no field file for simulation in space.

Examples

from tit.analyzer import run_group_analysis res = run_group_analysis( ... subject_ids=["ernie", "101"], simulation="L_Insula", space="voxel", ... analysis_type="spherical", center=(-35.0, 5.0, 5.0), radius=10.0, ... coordinate_space="MNI", ... ) # doctest: +SKIP res.summary_csv_path.name, sorted(res.subject_results) # doctest: +SKIP ('group_summary.csv', ['101', 'ernie']) run_group_analysis(["ernie", "101"], "L_Insula", space="mesh", ... analysis_type="cortical", atlas="DK40", ... region="lh.insula") # doctest: +SKIP

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",
    spheres: Sequence[tuple[float, float, float, float]] | None = None,
    atlas: str | None = None,
    region: str | list[str] | None = None,
    visualize: bool = False,
    output_dir: str | Path | None = None,
    field: str | None = None,
    mask_path: str | 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"``, ``"cortical"`` or ``"mask"``. 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 and mask). Default
        ``"subject"``; a group mask analysis requires ``"MNI"``.
    spheres : sequence of tuple of float or None, optional
        One ``(x, y, z, r)`` per sphere, unioned into a single ROI
        (spherical only). Takes precedence over *center*/*radius*.
    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.
    field : str or None, optional
        Field to analyze (``constants.FIELD_REGISTRY`` name, e.g.
        ``"TI_max"``, ``"TI_normal"``, ``"hf_peak"``). Default ``None``
        resolves the TI_max/mTI_max envelope per subject.
    mask_path : str or None, optional
        MNI-space NIfTI mask (``analysis_type="mask"`` only).

    Returns
    -------
    GroupResult
        Per-subject results, the summary CSV path
        (``group_summary.csv``), and the comparison plot path.

    Raises
    ------
    KeyError
        If *analysis_type* is not ``"spherical"``, ``"cortical"`` or
        ``"mask"``.
    ValueError
        If ``analysis_type="mask"`` with a non-MNI *coordinate_space* or an
        unreadable *mask_path*.
    FileNotFoundError
        If a subject has no field file for *simulation* in *space*.

    Examples
    --------
    >>> from tit.analyzer import run_group_analysis
    >>> res = run_group_analysis(
    ...     subject_ids=["ernie", "101"], simulation="L_Insula", space="voxel",
    ...     analysis_type="spherical", center=(-35.0, 5.0, 5.0), radius=10.0,
    ...     coordinate_space="MNI",
    ... )  # doctest: +SKIP
    >>> res.summary_csv_path.name, sorted(res.subject_results)  # doctest: +SKIP
    ('group_summary.csv', ['101', 'ernie'])
    >>> run_group_analysis(["ernie", "101"], "L_Insula", space="mesh",
    ...                    analysis_type="cortical", atlas="DK40",
    ...                    region="lh.insula")  # doctest: +SKIP

    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

    if analysis_type == "mask":
        from tit.opt.masks import validate_mask

        if coordinate_space.lower() != "mni":
            raise ValueError("Group mask analysis requires an MNI-space mask")
        validate_mask(mask_path)

    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_spheres(
                    spheres=spheres,
                    coordinate_space=coordinate_space,
                    visualize=visualize,
                )
                if spheres
                else a.analyze_sphere(
                    center=center,
                    radius=radius,
                    coordinate_space=coordinate_space,
                    visualize=visualize,
                )
            ),
            "mask": lambda a: a.analyze_mask(
                mask_path=mask_path,
                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, field=field)
            )

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