Skip to content

analyzer

tit.analyzer

TI-Toolbox analyzer — unified field analysis for mesh and voxel spaces.

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)

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.

GroupResult dataclass

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

Outcome of a multi-subject group analysis.

select_field_file

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

Return (field_path, field_name) for a given subject/simulation/space.

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

Parameters:

Name Type Description Default
subject_id str

Subject identifier (without sub- prefix).

required
simulation str

Simulation (montage) folder name.

required
space str

"mesh" or "voxel".

required

Returns:

Type Description
tuple[Path, str]

Tuple of (resolved field path, SimNIBS field name).

Raises:

Type Description
FileNotFoundError

If the expected field file does not exist.

ValueError

If space is not "mesh" or "voxel".

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 (field_path, field_name) for a given subject/simulation/space.

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

    Args:
        subject_id: Subject identifier (without ``sub-`` prefix).
        simulation: Simulation (montage) folder name.
        space: ``"mesh"`` or ``"voxel"``.

    Returns:
        Tuple of (resolved field path, SimNIBS field name).

    Raises:
        FileNotFoundError: If the expected field file does not exist.
        ValueError: If *space* is not ``"mesh"`` or ``"voxel"``.
    """
    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 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,
    )