Skip to content

field_selector

tit.analyzer.field_selector

Field selection utilities for automatic field file determination.

Resolves the correct field file path and SimNIBS field name for a given subject, simulation, and analysis space (mesh or voxel).

Public API

select_field_file Resolve the field path and SimNIBS field name for a subject/simulation. simulation_nifti_path Locate a named NIfTI of a TI or mTI simulation (used by the group tools).

See Also

tit.analyzer.analyzer : Analyzer class that consumes the resolved paths.

select_field_file

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

Return the field file path and SimNIBS field name.

Detects whether the simulation is TI (2 channels) or mTI (4+) 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". field : str or None, optional Field name from constants.FIELD_REGISTRY (e.g. "hf_peak"). Default None resolves :func:default_field: TI_max (TI), mTI_max (mTI), or hf_peak for a carrier-only control. "TI_max"/"mTI_max" are treated as aliases for the same quantity; the on-disk spelling is always chosen by the detected simulation type, regardless of which alias is passed.

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"/"voxel", field is unknown, or TI_normal is requested in voxel space (it has no NIfTI export).

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",
    field: str | None = None,
) -> tuple[Path, str]:
    """Return the field file path and SimNIBS field name.

    Detects whether the simulation is TI (2 channels) or mTI (4+) 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"``.
    field : str or None, optional
        Field name from ``constants.FIELD_REGISTRY`` (e.g. ``"hf_peak"``).
        Default ``None`` resolves :func:`default_field`: ``TI_max`` (TI), ``mTI_max``
        (mTI), or ``hf_peak`` for a carrier-only control.
        ``"TI_max"``/``"mTI_max"`` are treated as aliases for the same
        quantity; the on-disk spelling is always chosen by the detected
        simulation type, regardless of which alias is passed.

    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"``/``"voxel"``, *field* is unknown, or
        ``TI_normal`` is requested in voxel space (it has no NIfTI export).

    See Also
    --------
    Analyzer : Consumes the resolved path to load and analyze fields.
    """
    if field is not None:
        try:
            const.get_field_spec(field)
        except KeyError as exc:
            valid = ", ".join(const.get_field_names())
            raise ValueError(
                f"Unknown field: {field!r}. Valid fields: {valid}."
            ) from exc

    pm = get_path_manager()
    sim_dir = Path(pm.simulation(subject_id, simulation))
    is_mti = is_mti_simulation(subject_id, simulation)
    if field is None and sim_dir.is_dir():
        # Only a carrier-only run changes the default; the envelope keeps None, whose voxel
        # lookup still accepts an older run's unsuffixed NIfTI.
        if default_field(subject_id, simulation) == const.FIELD_HF_PEAK:
            field = const.FIELD_HF_PEAK

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

default_field

default_field(subject_id: str, simulation: str) -> str

The field an analysis with no field (the app's Auto) analyses.

The envelope -- TI_max, or mTI_max for an mTI run -- except for a carrier-only control (its run record's carrier_only), which has no envelope: there it is hf_peak. A run without a record is not carrier-only.

Raises:

Type Description
ValueError

the run record is not JSON.

Source code in tit/analyzer/field_selector.py
def default_field(subject_id: str, simulation: str) -> str:
    """The field an analysis with no field (the app's **Auto**) analyses.

    The envelope -- ``TI_max``, or ``mTI_max`` for an mTI run -- except for a carrier-only
    control (its run record's ``carrier_only``), which has no envelope: there it is
    ``hf_peak``. A run without a record is not carrier-only.

    Raises:
        ValueError: the run record is not JSON.
    """
    import json

    record = Path(get_path_manager().simulation(subject_id, simulation))
    record = record / "documentation" / "config.json"
    if record.is_file() and json.loads(record.read_text()).get("carrier_only") is True:
        return const.FIELD_HF_PEAK
    return (
        const.FIELD_MTI_MAX
        if is_mti_simulation(subject_id, simulation)
        else (const.FIELD_TI_MAX)
    )

is_mti_simulation

is_mti_simulation(subject_id: str, simulation: str) -> bool

Whether simulation is an mTI (multipolar) run.

The only signal is whether the simulation wrote an mTI/mesh/ directory; there is no flag stored anywhere else.

Parameters

subject_id : str Subject identifier (without sub- prefix). simulation : str Simulation (montage) folder name.

Returns

bool True for an mTI simulation, False for two-channel TI.

Source code in tit/analyzer/field_selector.py
def is_mti_simulation(subject_id: str, simulation: str) -> bool:
    """Whether *simulation* is an mTI (multipolar) run.

    The only signal is whether the simulation wrote an ``mTI/mesh/``
    directory; there is no flag stored anywhere else.

    Parameters
    ----------
    subject_id : str
        Subject identifier (without ``sub-`` prefix).
    simulation : str
        Simulation (montage) folder name.

    Returns
    -------
    bool
        ``True`` for an mTI simulation, ``False`` for two-channel TI.
    """
    sim_dir = Path(get_path_manager().simulation(subject_id, simulation))
    return (sim_dir / "mTI" / "mesh").is_dir()

simulation_nifti_path

simulation_nifti_path(subject_id: str, simulation: str, nifti_file_pattern: str) -> Path

The NIfTI nifti_file_pattern names in simulation, for TI and mTI alike.

Patterns use the TI spelling (grey_{simulation_name}_TI_MNI_MNI_TI_max.nii.gz, read from TI/niftis/). An mTI run writes the same files from its <sim>_mTI.msh into mTI/niftis/, and of its fields only the envelope is spelled differently (mTI_max; TI_avg, hf_peak and hf_rms keep their names). So for mTI the mesh token before the space (_TI_subject_ / _TI_MNI_) becomes _mTI_ and a trailing _TI_max becomes _mTI_max: grey_<sim>_mTI_MNI_MNI_mTI_max.nii.gz, grey_<sim>_mTI_MNI_MNI_TI_avg.nii.gz. A pattern already spelled for mTI is unchanged.

Parameters

subject_id : str Subject identifier (without sub- prefix). simulation : str Simulation (montage) folder name. nifti_file_pattern : str File name with {subject_id}/{simulation_name} placeholders.

Returns

pathlib.Path Where the file is (or would be); existence is not checked.

Source code in tit/analyzer/field_selector.py
def simulation_nifti_path(
    subject_id: str, simulation: str, nifti_file_pattern: str
) -> Path:
    """The NIfTI *nifti_file_pattern* names in *simulation*, for TI and mTI alike.

    Patterns use the TI spelling (``grey_{simulation_name}_TI_MNI_MNI_TI_max.nii.gz``,
    read from ``TI/niftis/``). An mTI run writes the same files from its ``<sim>_mTI.msh``
    into ``mTI/niftis/``, and of its fields only the envelope is spelled differently
    (``mTI_max``; ``TI_avg``, ``hf_peak`` and ``hf_rms`` keep their names). So for mTI the
    mesh token before the space (``_TI_subject_`` / ``_TI_MNI_``) becomes ``_mTI_`` and a
    trailing ``_TI_max`` becomes ``_mTI_max``: ``grey_<sim>_mTI_MNI_MNI_mTI_max.nii.gz``,
    ``grey_<sim>_mTI_MNI_MNI_TI_avg.nii.gz``. A pattern already spelled for mTI is unchanged.

    Parameters
    ----------
    subject_id : str
        Subject identifier (without ``sub-`` prefix).
    simulation : str
        Simulation (montage) folder name.
    nifti_file_pattern : str
        File name with ``{subject_id}``/``{simulation_name}`` placeholders.

    Returns
    -------
    pathlib.Path
        Where the file is (or would be); existence is not checked.
    """
    is_mti = is_mti_simulation(subject_id, simulation)
    if is_mti:
        nifti_file_pattern = _MTI_MESH_TOKEN.sub("_mTI_", nifti_file_pattern)
        nifti_file_pattern = _TI_MAX_SUFFIX.sub("_mTI_max", nifti_file_pattern)
    filename = nifti_file_pattern.format(
        subject_id=subject_id, simulation_name=simulation
    )
    sim_dir = Path(get_path_manager().simulation(subject_id, simulation))
    return sim_dir / ("mTI" if is_mti else "TI") / "niftis" / filename