Skip to content

fsaverage

tit.source.fsaverage

Project existing simulation field outputs onto an fsaverage template.

Complementary to :mod:tit.source.forward: where the forward pipeline lets a researcher reconstruct EEG sources onto fsaverage, this pipeline puts the stimulation fields on the same grid, so source activity and TI exposure can be compared vertexwise.

For an already-completed TI simulation it reads the run's own central surface (<TI|mTI>/mesh/surfaces/<sim>_<TI|mTI>_central.msh) and channel meshes and morphs the requested scalar fields to fsaverage:

  • TI_max -- orientation-maximized TI envelope |E| (central surface)
  • TI_normal -- directional TI envelope along the cortical normal (central surface)
  • hf_peak -- peak carrier field, max over channel sign combinations (CassarĂ  2025, safety)
  • hf_rms -- RMS carrier field over time, sqrt(tit.calc.hf_power) (V/m)

This path covers both standard TI and mTI. The two differ only in what the finished run is called on disk, and each resolver here takes either: :func:_ti_max_overlay reads TI_max from TI/mesh/surfaces or the same quantity spelled mTI_max under mTI/mesh/surfaces, :func:_ti_normal_surface reads TI_normal from the same surface, and :func:_carrier_volume_meshes collects every carrier volume mesh under high_Frequency/ -- TDCS_1/TDCS_2 for TI, and TDCS_A..TDCS_Z for mTI's N channels. hf_peak/hf_rms are then computed over all N channel fields, not a hardcoded two.

TI_max / TI_normal are read from the run's central surface; hf_peak / hf_rms are interpolated from the carrier volume meshes using the same formulas as the volume output (:func:tit.calc.hf_peak, :func:tit.calc.hf_power).

Unlike SimNIBS's native map_to_fsavg (which only runs at simulation time and only emits TI_max), this works post-hoc on any finished simulation and on the derived TI_normal / hf_peak / hf_rms quantities.

Output, in <sim>/fsaverage/: sub-<id>_sim-<sim>_space-fsaverage<N>_fields.msh -- the fsaverage central surfaces (lh nodes, then rh) with each field as a node field, openable in Gmsh or the Viewer -- plus its .msh.opt Gmsh view and a same-stem .json provenance sidecar (subject_id, simulation, carrier_only, fields).

Runs under simnibs_python (reads SimNIBS meshes)::

simnibs_python -m tit.source fsavg_config.json

See Also

tit.source.config.FsavgMapConfig : Parameter object. tit.source.forward.prepare_forward : The companion forward pipeline.

write_fsaverage_msh

write_fsaverage_msh(path: Path, maps: dict[str, ndarray], spacing: int, provenance: dict) -> None

Write [lh; rh] fsaverage values as a viewable SimNIBS surface mesh.

Follows SimNIBS's own fsavg_overlays/<name>_fsavg.msh (join_and_write in :func:simnibs.utils.transformations.middle_gm_interpolation): the template central surfaces joined lh then rh, one node field per quantity, and the Gmsh .opt view beside it. The template shares its vertex numbering with the sphere :func:cross_subject_map resamples onto, so value i lands on node i (tests/numerical/test_fsaverage_msh.py). .msh carries no free metadata, so provenance goes to a same-stem .json sidecar (BIDS style).

Source code in tit/source/fsaverage.py
def write_fsaverage_msh(
    path: Path, maps: dict[str, np.ndarray], spacing: int, provenance: dict
) -> None:
    """Write ``[lh; rh]`` fsaverage values as a viewable SimNIBS surface mesh.

    Follows SimNIBS's own ``fsavg_overlays/<name>_fsavg.msh`` (``join_and_write`` in
    :func:`simnibs.utils.transformations.middle_gm_interpolation`): the template
    central surfaces joined lh then rh, one node field per quantity, and the Gmsh
    ``.opt`` view beside it.  The template shares its vertex numbering with the
    sphere :func:`cross_subject_map` resamples onto, so value ``i`` lands on node
    ``i`` (``tests/numerical/test_fsaverage_msh.py``).  ``.msh`` carries no free
    metadata, so *provenance* goes to a same-stem ``.json`` sidecar (BIDS style).
    """
    from simnibs.mesh_tools import mesh_io

    surfs = mesh_io.load_fsaverage_template("central", spacing)
    mesh = surfs["lh"].join_mesh(surfs["rh"])
    for name, values in maps.items():
        mesh.add_node_field(np.asarray(values, dtype=float), name)
    path.parent.mkdir(parents=True, exist_ok=True)
    mesh_io.write_msh(mesh, str(path))
    mesh.view(visible_fields=next(iter(maps))).write_opt(str(path))
    path.with_suffix(".json").write_text(
        json.dumps({**provenance, "fields": list(maps)}, indent=2) + "\n"
    )

read_fsaverage_fields

read_fsaverage_fields(path: Path) -> dict[str, ndarray]

Every node field of a mesh written by :func:write_fsaverage_msh.

Uses SimNIBS's reader: every caller (tit.stats, tit.plotting, scripts) runs under simnibs_python, like the writer.

Source code in tit/source/fsaverage.py
def read_fsaverage_fields(path: Path) -> dict[str, np.ndarray]:
    """Every node field of a mesh written by :func:`write_fsaverage_msh`.

    Uses SimNIBS's reader: every caller (``tit.stats``, ``tit.plotting``,
    scripts) runs under ``simnibs_python``, like the writer.
    """
    from simnibs.mesh_tools import mesh_io

    mesh = mesh_io.read_msh(str(path))
    return {
        name: np.asarray(data.value, dtype=np.float64).reshape(-1)
        for name, data in mesh.field.items()
    }

read_fsaverage_field

read_fsaverage_field(path: Path, field: str) -> ndarray

One node field of a mesh written by :func:write_fsaverage_msh.

Source code in tit/source/fsaverage.py
def read_fsaverage_field(path: Path, field: str) -> np.ndarray:
    """One node field of a mesh written by :func:`write_fsaverage_msh`."""
    fields = read_fsaverage_fields(path)
    if field not in fields:
        raise KeyError(
            f"{path.name} has no field {field!r}; available: {sorted(fields)}"
        )
    return fields[field]

project_subject

project_subject(subject_id: str, sim: str, cfg: FsavgMapConfig) -> tuple[str, str, str]

Project one (subject, simulation) and write it as an fsaverage .msh.

Returns (subject_id, status, message) where status is one of {"ok", "partial", "cached", "failed"} so batch runs can record and continue; "partial" means some requested fields could not be projected.

Source code in tit/source/fsaverage.py
def project_subject(
    subject_id: str,
    sim: str,
    cfg: FsavgMapConfig,
) -> tuple[str, str, str]:
    """Project one (subject, simulation) and write it as an fsaverage ``.msh``.

    Returns ``(subject_id, status, message)`` where status is one of
    ``{"ok", "partial", "cached", "failed"}`` so batch runs can record and
    continue; "partial" means some requested fields could not be projected.
    """
    from dataclasses import replace

    from tit.source.config import VALID_FSAVG_FIELDS
    from tit.tools.msh_fields import msh_field_names

    pm = get_path_manager()
    out_path = Path(pm.sim_fsaverage_fields(subject_id, sim, cfg.fsaverage_spacing))
    try:
        carrier_only = _recorded_carrier_only(pm, subject_id, sim)
        if carrier_only:
            # A carrier-only control has no TI envelope: TI_max / TI_normal cannot
            # exist. The default (all-fields) request means "what this run has";
            # an explicit selection naming a TI field is refused.
            ti = {"TI_max", "TI_normal"} & set(cfg.fields)
            if ti and tuple(cfg.fields) != VALID_FSAVG_FIELDS:
                raise ValueError(
                    f"{sorted(ti)} cannot be projected: {sim} is a carrier-only run "
                    "with no TI envelope; request hf_peak / hf_rms."
                )
            cfg = replace(cfg, fields=tuple(f for f in cfg.fields if f not in ti))
        if out_path.exists() and not cfg.overwrite:
            names = msh_field_names(out_path)  # None: could not tell, keep the file
            if names is None or set(cfg.fields) <= names:
                return subject_id, "cached", out_path.name
        maps = _compute_fields(pm, subject_id, sim, cfg, carrier_only)
    except Exception as exc:  # noqa: BLE001 - record per-subject and continue
        return subject_id, "failed", repr(exc)
    write_fsaverage_msh(
        out_path,
        maps,
        cfg.fsaverage_spacing,
        {"subject_id": subject_id, "simulation": sim, "carrier_only": carrier_only},
    )
    medians = ", ".join(f"{k} med={np.median(v):.3f}" for k, v in maps.items())
    missing = [f for f in cfg.fields if f not in maps]
    if missing:
        return subject_id, "partial", f"{medians} -> {out_path.name}; missing {missing}"
    return subject_id, "ok", f"{medians} -> {out_path.name}"

project_fields_to_fsaverage

project_fields_to_fsaverage(subjects: list[tuple[str, str]], cfg: FsavgMapConfig) -> list[tuple[str, str, str]]

Project field outputs to fsaverage for many (subject, simulation) pairs.

Parameters

subjects : list of (str, str) (subject_id, simulation_name) pairs to project. cfg : FsavgMapConfig Field selection, fsaverage spacing, worker count, overwrite flag.

Returns

list of (str, str, str) Per-pair (subject_id, status, message) results.

Source code in tit/source/fsaverage.py
def project_fields_to_fsaverage(
    subjects: list[tuple[str, str]],
    cfg: FsavgMapConfig,
) -> list[tuple[str, str, str]]:
    """Project field outputs to fsaverage for many (subject, simulation) pairs.

    Parameters
    ----------
    subjects : list of (str, str)
        ``(subject_id, simulation_name)`` pairs to project.
    cfg : FsavgMapConfig
        Field selection, fsaverage spacing, worker count, overwrite flag.

    Returns
    -------
    list of (str, str, str)
        Per-pair ``(subject_id, status, message)`` results.
    """
    workers = max(1, min(cfg.workers, len(subjects))) if subjects else 1
    logger.info("Projecting %d simulation(s) with %d worker(s)", len(subjects), workers)

    results: list[tuple[str, str, str]] = []
    if workers == 1:
        for subject_id, sim in subjects:
            res = project_subject(subject_id, sim, cfg)
            _log_result(res)
            results.append(res)
    else:
        with ProcessPoolExecutor(max_workers=workers) as pool:
            futures = {
                pool.submit(project_subject, sid, sim, cfg): (sid, sim)
                for sid, sim in subjects
            }
            for future in as_completed(futures):
                res = future.result()
                _log_result(res)
                results.append(res)
    return results