Skip to content

volume_fields

tit.volume_fields

A simulation's volume fields: computed by the simulator, produced again on demand.

:func:compute_volume_fields is the one per-field computation. The simulator writes its output into the montage's volume mesh (:meth:tit.sim.base.BaseSimulation._write_volume_mesh), and the on-demand path below produces a field the simulation did not save from the same inputs: the channel meshes every run keeps in high_Frequency/mesh/ (<sid>_TDCS_<k>_<cond>.msh, k = 1, 2 for TI and A..Z for mTI), cropped to brain tissue as the simulator crops them.

What persists, so the next step finds the field saved:

  • :func:ensure_mesh_fields adds the field to the volume mesh <sim>/<TI|mTI>/mesh/<sim>_<TI|mTI>.msh and rewrites its central surface <sim>/<TI|mTI>/mesh/surfaces/<sim>_<TI|mTI>_central.msh (:func:central_surface: every volume field and TI_normal as node data), which is what a mesh analysis reads. It also builds that surface for a run that lacks it or whose surface lacks a field -- a run made before the surface carried TI_normal -- taking TI_normal from the run's old <sim>_normal.msh (<sim>_mTI_normal.msh) when it has one, else from the channel meshes. It never deletes an old run's files; nothing reads them;
  • :func:ensure_niftis / :func:ensure_nifti_file also write the NIfTI a voxel step reads, under the simulator's own name (<tissue_><sim>_<TI|mTI>_subject_<field>.nii.gz, or ..._MNI_MNI_<field>.nii.gz in MNI space) in <sim>/<TI|mTI>/niftis/; a grey_ or white_ NIfTI is converted from the volume mesh cropped to that tissue (:data:tit.tools.mesh2nii.TISSUE_TAGS).

NIfTIs and the mesh stay consistent. A run that wrote NIfTIs (map_to_vol in its run record) has one per mesh field, so a field added to its mesh gets its NIfTIs too -- every converted mesh, subject space and, with map_to_mni, MNI space -- and the set still equals the mesh field set. A run with NIfTIs off has no such set: its niftis/ holds only what voxel steps asked for, each a NIfTI of a field its mesh carries, never one the mesh lacks.

Writes are crash-safe: everything is staged in a hidden folder and moved into place with atomic renames, derived files first and the volume mesh last (:func:_produce_mesh_fields).

Each says one plain sentence per production before doing it ("hf_peak and hf_rms were not saved for L_Insula; computing them from the channel fields and saving them to L_Insula_TI.msh."), through the caller's say (its own logger.info, so it reaches that job's output), and returns the sentences so the caller can show them wherever its results are read. Nothing is guessed: a field that cannot be produced (no channel meshes, no recorded config, an envelope of a carrier-only run, hf_power_density whose conductivity is not kept) raises :class:FieldUnavailableError naming what is missing.

FieldUnavailableError

Bases: FileNotFoundError

A field a step needs was not saved and cannot be produced from what the simulation kept.

hf_power_density

hf_power_density(fields, sigma, *, coherent: bool = False) -> ndarray

Absorbed (Joule) power density of the carriers, time-averaged, W/m^3.

(1/2) sum_c E_c^T sigma E_c for zero-to-peak carrier fields E_c (V/m) at distinct frequencies: sigma * hf_power for isotropic sigma (N,) S/m, the full tensor form for (N, 9) anisotropic conductivities (J . E averaged over time). Not SAR (that is this over the tissue density, IEEE C95.1-2019 p. 35). coherent is the carrier-only control: one sinusoid of amplitude sum_c E_c.

Source code in tit/volume_fields.py
def hf_power_density(fields, sigma, *, coherent: bool = False) -> np.ndarray:
    """Absorbed (Joule) power density of the carriers, time-averaged, W/m^3.

    ``(1/2) sum_c E_c^T sigma E_c`` for zero-to-peak carrier fields ``E_c``
    (V/m) at distinct frequencies: ``sigma * hf_power`` for isotropic
    ``sigma`` ``(N,)`` S/m, the full tensor form for ``(N, 9)`` anisotropic
    conductivities (``J . E`` averaged over time).  Not SAR (that is this over the
    tissue density, IEEE C95.1-2019 p. 35).  *coherent* is the carrier-only control: one
    sinusoid of amplitude ``sum_c E_c``.
    """
    sigma = np.asarray(sigma, dtype=np.float64)
    if sigma.ndim == 1:
        return sigma * hf_power(fields, coherent=coherent)
    E = np.stack([np.asarray(f, dtype=np.float64) for f in fields])
    if coherent:
        E = E.sum(axis=0, keepdims=True)
    return 0.5 * np.einsum("fni,nij,fnj->n", E, sigma.reshape(-1, 3, 3), E)

compute_volume_fields

compute_volume_fields(e_fields: list, names, *, max_name: str, coherent: bool, sigma=None) -> list[tuple[str, ndarray]]

(on-disk name, values) of each selected volume field, over all channel fields jointly.

Parameters:

Name Type Description Default
e_fields list

The channels' E on the brain-cropped mesh, in channel order (channels 2k-1 and 2k form beating pair k).

required
names

Logical field names (:data:tit.constants.SELECTABLE_OUTPUT_FIELDS); written in registry order whatever order they come in.

required
max_name str

The envelope maximum's on-disk name (TI_max; mTI_max for mTI).

required
coherent bool

Carrier-only control: the channel fields add coherently.

required
sigma

Per-element conductivity of the solve, for hf_power_density only.

None
Source code in tit/volume_fields.py
def compute_volume_fields(
    e_fields: list,
    names,
    *,
    max_name: str,
    coherent: bool,
    sigma=None,
) -> list[tuple[str, np.ndarray]]:
    """``(on-disk name, values)`` of each selected volume field, over all channel fields jointly.

    Args:
        e_fields: The channels' ``E`` on the brain-cropped mesh, in channel order (channels
            2k-1 and 2k form beating pair k).
        names: Logical field names (:data:`tit.constants.SELECTABLE_OUTPUT_FIELDS`); written
            in registry order whatever order they come in.
        max_name: The envelope maximum's on-disk name (``TI_max``; ``mTI_max`` for mTI).
        coherent: Carrier-only control: the channel fields add coherently.
        sigma: Per-element conductivity of the solve, for ``hf_power_density`` only.
    """
    selected = set(names)
    out = []
    for name in _FIELD_ORDER:
        if name not in selected:
            continue
        if name == const.FIELD_TI_MAX:
            out.append((max_name, envelope_max(e_fields)))
        elif name == const.FIELD_TI_AVG:
            out.append((name, envelope_mean(e_fields)))
        elif name == const.FIELD_HF_PEAK:
            # Peak carrier field (CassarĂ  2025 Part I, Eq. 3); each channel field enters on its own.
            out.append((name, hf_peak(e_fields, coherent=coherent)))
        elif name == const.FIELD_HF_RMS:
            # RMS carrier field over time: power adds across carriers (CassarĂ  2025).
            out.append((name, np.sqrt(hf_power(e_fields, coherent=coherent))))
        else:
            out.append((name, hf_power_density(e_fields, sigma, coherent=coherent)))
    return out

central_surface

central_surface(volume, m2m_dir, *, e_fields=None)

The subject's central cortical surface (lh then rh) carrying volume's fields as node data.

What SimNIBS's msh2cortex does, in process: the brain-tissue volume mesh is cropped to white matter, grey matter and CSF, and its fields are interpolated onto the m2m central surfaces inside the grey-matter tetrahedra (ElementData.interpolate_to_surface), all of them in one stacked call per hemisphere; the hemispheres are joined as msh2cortex joins them (tags LH_/RH_CENTRAL_SURFACE). With e_fields -- the channels' E on volume's elements, in channel order -- their interpolated vectors give TI_normal, the envelope along the node normals (:func:tit.calc.envelope), as the old surface overlays' E_normal gave it (tests/numerical/test_central_surface_real.py: both to 3e-14 V/m).

Raises:

Type Description
ValueError

volume carries a field that is not a scalar.

Source code in tit/volume_fields.py
def central_surface(volume, m2m_dir, *, e_fields=None):
    """The subject's central cortical surface (lh then rh) carrying *volume*'s fields as node data.

    What SimNIBS's ``msh2cortex`` does, in process: the brain-tissue *volume* mesh is cropped to
    white matter, grey matter and CSF, and its fields are interpolated onto the m2m central
    surfaces inside the grey-matter tetrahedra (``ElementData.interpolate_to_surface``), all of
    them in one stacked call per hemisphere; the hemispheres are joined as ``msh2cortex`` joins
    them (tags ``LH_/RH_CENTRAL_SURFACE``). With *e_fields* -- the channels' ``E`` on
    *volume*'s elements, in channel order -- their interpolated vectors give ``TI_normal``, the
    envelope along the node normals (:func:`tit.calc.envelope`), as the old surface overlays'
    ``E_normal`` gave it (tests/numerical/test_central_surface_real.py: both to 3e-14 V/m).

    Raises:
        ValueError: *volume* carries a field that is not a scalar.
    """
    from simnibs import mesh_io
    from simnibs.mesh_tools.mesh_io import ElementTags
    from simnibs.utils.file_finder import SubjectFiles

    # Only registered fields: an old run's retired field is never carried forward.
    kept = [d for d in volume.elmdata if d.field_name in _REGISTERED]
    names = [d.field_name for d in kept]
    columns = []
    for data in kept:
        if data.nr_comp != 1:
            raise ValueError(
                f"the field mesh carries {data.field_name!r} with {data.nr_comp} components; "
                "a volume field is a scalar"
            )
        columns.append(np.asarray(data.value, dtype=np.float64).reshape(-1, 1))
    columns += [np.asarray(e, dtype=np.float64).reshape(-1, 3) for e in e_fields or ()]
    values = np.hstack(columns)
    stacked = mesh_io.Msh(volume.nodes, volume.elm)
    # SimNIBS interpolates an (N, 1) block wrongly; one column goes in as the scalar it is.
    stacked.add_element_field(
        values[:, 0] if values.shape[1] == 1 else values, "stacked"
    )
    stacked = stacked.crop_mesh(tags=[ElementTags.WM, ElementTags.GM, ElementTags.CSF])
    gm = stacked.elm.get_tags(ElementTags.GM, return_element_numbers=True)

    subject = SubjectFiles(subpath=str(m2m_dir))
    surfaces = mesh_io.load_subject_surfaces(subject, "central")
    values = np.vstack(
        [
            np.asarray(
                stacked.field["stacked"]
                .interpolate_to_surface(surfaces[hemi], th_indices=gm)
                .value
            ).reshape(surfaces[hemi].nodes.nr, -1)
            for hemi in ("lh", "rh")
        ]
    )
    for hemi, tag in (
        ("lh", ElementTags.LH_CENTRAL_SURFACE),
        ("rh", ElementTags.RH_CENTRAL_SURFACE),
    ):
        surfaces[hemi].elm.tag1 = tag * np.ones(surfaces[hemi].elm.nr, dtype=int)
        surfaces[hemi].elm.tag2 = tag * np.ones(surfaces[hemi].elm.nr, dtype=int)
    surface = surfaces["lh"].join_mesh(surfaces["rh"])
    surface.nodedata, surface.elmdata = [], []
    for i, name in enumerate(names):
        surface.add_node_field(values[:, i], name)
    if e_fields:
        k = len(names)
        vectors = [values[:, k + 3 * c : k + 3 * c + 3] for c in range(len(e_fields))]
        normals = surface.nodes_normals().value
        surface.add_node_field(envelope(vectors, normals), const.FIELD_TI_NORMAL)
    return surface

check_surface_producible

check_surface_producible(subject_id: str, simulation: str) -> None

Raise :class:FieldUnavailableError if the central surface is incomplete and cannot be built.

Filesystem checks only (headers and file existence), for a job's preflight.

Source code in tit/volume_fields.py
def check_surface_producible(subject_id: str, simulation: str) -> None:
    """Raise :class:`FieldUnavailableError` if the central surface is incomplete and cannot be built.

    Filesystem checks only (headers and file existence), for a job's preflight.
    """
    sim = _simulation(subject_id, simulation)
    _missing(sim, [])  # the volume mesh must be there and readable
    if _surface_lacks(sim) != []:
        _check_surface_producible(sim, _recorded_config(sim))

check_producible

check_producible(subject_id: str, simulation: str, fields) -> None

Raise :class:FieldUnavailableError if a field of fields is neither saved nor producible.

Filesystem checks only (no mesh is loaded), for a job's preflight.

Source code in tit/volume_fields.py
def check_producible(subject_id: str, simulation: str, fields) -> None:
    """Raise :class:`FieldUnavailableError` if a field of *fields* is neither saved nor producible.

    Filesystem checks only (no mesh is loaded), for a job's preflight.
    """
    sim = _simulation(subject_id, simulation)
    missing = _missing(sim, _validated(fields))
    if missing:
        _check_producible(sim, missing)

ensure_mesh_fields

ensure_mesh_fields(subject_id: str, simulation: str, fields, *, say: Callable[[str], None] = _quiet, surface: bool = True) -> list[str]

Make the volume mesh carry every field of fields, and its central surface all of them.

A missing field is computed with :func:compute_volume_fields from the channel meshes and added to the volume mesh, and the central surface is rewritten from it with every field. A central surface that is missing or lacks a field of the volume mesh or TI_normal is rebuilt even when fields is empty (an old run, :func:_produce_surface). Returns one sentence per production (empty when nothing was needed), each handed to say before the work it announces starts.

Parameters:

Name Type Description Default
subject_id str

Subject identifier (no sub-).

required
simulation str

Simulation (montage) folder name.

required
fields

Logical or on-disk volume field names (TI_max and mTI_max alike); may be empty, to make sure of the central surface only.

required
say Callable[[str], None]

Called with each sentence first (a caller's logger.info).

_quiet
surface bool

Also rebuild an incomplete central surface when no field is missing. False for a step that reads only NIfTIs (:func:ensure_niftis).

True

Raises:

Type Description
FieldUnavailableError

a missing field or the surface cannot be produced.

ValueError

a name is not a volume field (TI_normal is a surface field).

RuntimeError

the head model's central surface is not the one the run used.

Source code in tit/volume_fields.py
def ensure_mesh_fields(
    subject_id: str,
    simulation: str,
    fields,
    *,
    say: Callable[[str], None] = _quiet,
    surface: bool = True,
) -> list[str]:
    """Make the volume mesh carry every field of *fields*, and its central surface all of them.

    A missing field is computed with :func:`compute_volume_fields` from the channel meshes and
    added to the volume mesh, and the central surface is rewritten from it with every field.
    A central surface that is missing or lacks a field of the volume mesh or ``TI_normal`` is
    rebuilt even when *fields* is empty (an old run, :func:`_produce_surface`). Returns one
    sentence per production (empty when nothing was needed), each handed to *say* before the
    work it announces starts.

    Args:
        subject_id: Subject identifier (no ``sub-``).
        simulation: Simulation (montage) folder name.
        fields: Logical or on-disk volume field names (``TI_max`` and ``mTI_max`` alike); may be
            empty, to make sure of the central surface only.
        say: Called with each sentence first (a caller's ``logger.info``).
        surface: Also rebuild an incomplete central surface when no field is missing.
            ``False`` for a step that reads only NIfTIs (:func:`ensure_niftis`).

    Raises:
        FieldUnavailableError: a missing field or the surface cannot be produced.
        ValueError: a name is not a volume field (``TI_normal`` is a surface field).
        RuntimeError: the head model's central surface is not the one the run used.
    """
    sim = _simulation(subject_id, simulation)
    names = _validated(fields)
    if not _missing(sim, names) and (not surface or _surface_lacks(sim) == []):
        return []
    with _producing(sim):
        missing = _missing(sim, names)  # another producer may have finished meanwhile
        if missing:
            config = _check_producible(sim, missing)
            names = [_on_disk(sim, n) for n in missing]
            was, it = _was(names)
            spaces = _recorded_nifti_spaces(config)
            also = ""
            if spaces:
                kind = "subject and MNI-space NIfTIs" if "mni" in spaces else "NIfTIs"
                also = f", with the {kind} this run writes for every field"
            notice = (
                f"{_listed(names)} {was} not saved for {sim.name}; computing {it} from the "
                f"channel fields and saving {it} to {sim.volume_mesh.name}{also}."
            )
            say(notice)
            _produce_mesh_fields(sim, config, missing, names, spaces)
            return [notice]
        if not surface:
            return []
        lacks = _surface_lacks(sim)
        if lacks == []:
            return []
        config = _recorded_config(sim)
        _check_surface_producible(sim, config)
        what = (
            "has no central surface"
            if lacks is None
            else (f"has a central surface without {_listed(lacks)}")
        )
        notice = f"{sim.name} {what}; writing {sim.central.name} from its field mesh."
        say(notice)
        _produce_surface(sim, config)
    return [notice]

ensure_niftis

ensure_niftis(subject_id: str, simulation: str, fields, *, tissue: str | None, space: str = 'subject', say: Callable[[str], None] = _quiet) -> tuple[dict[str, Path], list[str]]

The NIfTI of each field of fields for one tissue and space, produced if not saved.

Parameters:

Name Type Description Default
subject_id str

Subject identifier (no sub-).

required
simulation str

Simulation (montage) folder name.

required
fields

Volume field names, as for :func:ensure_mesh_fields.

required
tissue str | None

"grey", "white" or None (the whole brain-tissue mesh); a tissue's NIfTI is converted from the volume mesh cropped to it.

required
space str

"subject" or "mni".

'subject'
say Callable[[str], None]

As for :func:ensure_mesh_fields.

_quiet

Returns:

Type Description
tuple[dict[str, Path], list[str]]

{on-disk field name: path} and the sentences said while producing them.

Raises:

Type Description
FieldUnavailableError

a missing field cannot be produced.

RuntimeError

the conversion ran but did not write a requested file.

Source code in tit/volume_fields.py
def ensure_niftis(
    subject_id: str,
    simulation: str,
    fields,
    *,
    tissue: str | None,
    space: str = "subject",
    say: Callable[[str], None] = _quiet,
) -> tuple[dict[str, Path], list[str]]:
    """The NIfTI of each field of *fields* for one tissue and space, produced if not saved.

    Args:
        subject_id: Subject identifier (no ``sub-``).
        simulation: Simulation (montage) folder name.
        fields: Volume field names, as for :func:`ensure_mesh_fields`.
        tissue: ``"grey"``, ``"white"`` or ``None`` (the whole brain-tissue mesh); a tissue's
            NIfTI is converted from the volume mesh cropped to it.
        space: ``"subject"`` or ``"mni"``.
        say: As for :func:`ensure_mesh_fields`.

    Returns:
        ``{on-disk field name: path}`` and the sentences said while producing them.

    Raises:
        FieldUnavailableError: a missing field cannot be produced.
        RuntimeError: the conversion ran but did not write a requested file.
    """
    if tissue not in ("grey", "white", None) or space not in ("subject", "mni"):
        raise ValueError(
            f"tissue {tissue!r} / space {space!r} is not one a simulation writes"
        )
    sim = _simulation(subject_id, simulation)
    names = _validated(fields)
    base = f"{tissue + '_' if tissue else ''}{sim.name}_{sim.mode}"
    paths = {
        _on_disk(sim, n): sim.niftis_dir / _nifti_name(base, space, _on_disk(sim, n))
        for n in names
    }
    if all(_saved(p) for p in paths.values()):
        return {f: _saved(p) for f, p in paths.items()}, []
    notices = ensure_mesh_fields(subject_id, simulation, names, say=say, surface=False)
    with _producing(sim):
        absent = [f for f, p in paths.items() if not _saved(p)]
        if absent:
            mesh = sim.volume_mesh
            was, it = _was(absent)
            kind = "an MNI-space NIfTI" if space == "mni" else "a NIfTI"
            if len(absent) > 1:
                kind = "MNI-space NIfTIs" if space == "mni" else "NIfTIs"
            notice = (
                f"{_listed(absent)} {was} not saved as {kind} for {sim.name}; writing "
                f"{it} from {mesh.name}{f' ({tissue} matter)' if tissue else ''}."
            )
            say(notice)
            notices.append(notice)
            from tit.paths import get_path_manager

            m2m = get_path_manager().m2m(sim.subject_id)
            with _staging(sim) as stage:
                _convert(mesh, m2m, stage, base, space, absent, tissue)
                _move_all(stage, sim.niftis_dir)
    return {f: _saved(p) or p for f, p in paths.items()}, notices

voxelise

voxelise(mesh, m2m, out_dir, fields, *, tissue=None) -> dict[str, Path]

Subject-space NIfTIs of fields of the field mesh mesh (cropped to tissue).

Written into out_dir as <tissue_><mesh stem>_subject_<field>.nii.gz.

The conversion the on-demand path uses, for a caller that keeps the volumes elsewhere (the simulation report voxelises its envelope into a scratch folder). Keyed by field.

Raises:

Type Description
FieldUnavailableError

mesh is missing, or does not carry one of fields.

RuntimeError

the conversion did not write a requested file.

Source code in tit/volume_fields.py
def voxelise(mesh, m2m, out_dir, fields, *, tissue=None) -> dict[str, Path]:
    """Subject-space NIfTIs of *fields* of the field mesh *mesh* (cropped to *tissue*).

    Written into *out_dir* as ``<tissue_><mesh stem>_subject_<field>.nii.gz``.

    The conversion the on-demand path uses, for a caller that keeps the volumes elsewhere
    (the simulation report voxelises its envelope into a scratch folder). Keyed by field.

    Raises:
        FieldUnavailableError: *mesh* is missing, or does not carry one of *fields*.
        RuntimeError: the conversion did not write a requested file.
    """
    mesh = Path(mesh)
    carried = msh_field_names(mesh) if mesh.is_file() else None
    absent = [f for f in fields if carried is None or f not in carried]
    if absent:
        raise FieldUnavailableError(
            f"{mesh} is missing or does not carry {_listed(absent)}, so it cannot be "
            "voxelised."
        )
    base = f"{tissue + '_' if tissue else ''}{mesh.stem}"
    written = _convert(mesh, m2m, Path(out_dir), base, "subject", fields, tissue)
    return dict(zip(fields, written))

check_nifti_producible

check_nifti_producible(subject_id: str, simulation: str, path) -> bool

Whether path is a NIfTI this simulation writes and could produce now (raises if not).

False when the name is not one the simulator writes (the caller's own missing-file message applies); raises :class:FieldUnavailableError when it is, but its field cannot be produced (no field mesh, no channel meshes, ...).

Source code in tit/volume_fields.py
def check_nifti_producible(subject_id: str, simulation: str, path) -> bool:
    """Whether *path* is a NIfTI this simulation writes and could produce now (raises if not).

    ``False`` when the name is not one the simulator writes (the caller's own missing-file
    message applies); raises :class:`FieldUnavailableError` when it is, but its field cannot
    be produced (no field mesh, no channel meshes, ...).
    """
    found = _nifti_at(subject_id, simulation, Path(path))
    if found is None:
        return False
    sim, (_, _, field) = found
    try:
        missing = _missing(sim, [field])
        if missing:
            _check_producible(sim, missing)
    except FieldUnavailableError as exc:
        raise FieldUnavailableError(
            f"{path} was not saved and cannot be written: {exc}"
        )
    return True

ensure_nifti_file

ensure_nifti_file(subject_id: str, simulation: str, path, *, say: Callable[[str], None] = _quiet) -> list[str]

Produce the simulation NIfTI path if it is missing; the sentences said.

path is a name the simulator writes (simulation_nifti_path resolves the group tools' patterns to one) of a simulation that has its field mesh. Any other missing file -- another name, a simulation that is not there -- is left to the caller to report as missing.

Raises:

Type Description
FieldUnavailableError

the field mesh is there, but the field cannot be produced.

Source code in tit/volume_fields.py
def ensure_nifti_file(
    subject_id: str, simulation: str, path, *, say: Callable[[str], None] = _quiet
) -> list[str]:
    """Produce the simulation NIfTI *path* if it is missing; the sentences said.

    *path* is a name the simulator writes (``simulation_nifti_path`` resolves the group tools'
    patterns to one) of a simulation that has its field mesh. Any other missing file -- another
    name, a simulation that is not there -- is left to the caller to report as missing.

    Raises:
        FieldUnavailableError: the field mesh is there, but the field cannot be produced.
    """
    path = Path(path)
    if path.is_file():
        return []
    found = _nifti_at(subject_id, simulation, path)
    if found is None or not found[0].volume_mesh.is_file():
        return []
    tissue, space, field = found[1]
    try:
        return ensure_niftis(
            subject_id, simulation, [field], tissue=tissue, space=space, say=say
        )[1]
    except FieldUnavailableError as exc:
        raise FieldUnavailableError(
            f"{path} was not saved and cannot be written: {exc}"
        )

check_niftis_match_meshes

check_niftis_match_meshes(volume_mesh, niftis_dir, *, mni: bool) -> None

Raise unless volume_mesh has exactly one NIfTI per field, per tissue and space.

The simulator's NIfTI set is its mesh field set, name for name: <base>_subject_<field> (and <base>_MNI_MNI_<field> with mni) for each field, where <base> is the volume mesh's stem and its grey_ / white_ crops, and nothing else under those names.

Raises:

Type Description
RuntimeError

the mesh's fields cannot be read, or the NIfTIs differ from them.

Source code in tit/volume_fields.py
def check_niftis_match_meshes(volume_mesh, niftis_dir, *, mni: bool) -> None:
    """Raise unless *volume_mesh* has exactly one NIfTI per field, per tissue and space.

    The simulator's NIfTI set is its mesh field set, name for name: ``<base>_subject_<field>``
    (and ``<base>_MNI_MNI_<field>`` with *mni*) for each field, where ``<base>`` is the volume
    mesh's stem and its ``grey_`` / ``white_`` crops, and nothing else under those names.

    Raises:
        RuntimeError: the mesh's fields cannot be read, or the NIfTIs differ from them.
    """
    volume_mesh, niftis_dir = Path(volume_mesh), Path(niftis_dir)
    carried = msh_field_names(volume_mesh)
    if carried is None:
        raise RuntimeError(
            f"Cannot read the fields of {volume_mesh} to check its NIfTIs."
        )
    carried &= _REGISTERED  # a retired field on an old mesh has no NIfTI to match
    written = [p.name for p in niftis_dir.glob("*.nii*")]
    problems = []
    for base in (f"{t}{volume_mesh.stem}" for t in ("", "grey_", "white_")):
        for token in ("subject", "MNI_MNI") if mni else ("subject",):
            prefix = f"{base}_{token}_"
            have = {
                n[len(prefix) :].split(".nii")[0]
                for n in written
                if n.startswith(prefix)
            }
            if have != carried:
                problems.append(
                    f"{base} carries {sorted(carried)} but its {token} NIfTIs are "
                    f"{sorted(have)}"
                )
    if problems:
        raise RuntimeError(
            "The NIfTIs do not match the field mesh: " + "; ".join(problems) + "."
        )