Skip to content

sim

tit.sim

TI/mTI simulation engine.

This package implements temporal interference (TI) and multi-channel temporal interference (mTI) brain stimulation simulations. It wraps the SimNIBS finite-element solver, providing electrode configuration, field computation, and BIDS-compliant output organization.

Public API

BaseSimulation Abstract base class for TI/mTI simulation pipelines. SimulationConfig Dataclass holding all parameters for a simulation run. Montage Dataclass describing a named electrode montage. SimulationMode Enum distinguishing TI (2-pair) from mTI (4+-pair) mode. parse_intensities Parse a comma-separated intensity string into a float list. run_simulation Execute simulations for every montage in a configuration. load_montages Load named montages from the project's montage_list.json. list_montage_names List all montage names defined under an EEG net. load_montage_data Load the full montage_list.json as a dict. save_montage_data Write a montage dict to montage_list.json. ensure_montage_file Return (and optionally create) the path to montage_list.json. upsert_montage Insert or update a montage definition in montage_list.json.

See Also

tit.sim.config : Configuration dataclasses and enums. tit.sim.utils : Orchestration, montage I/O, and post-processing helpers. tit.sim.TI : 2-pair TI simulation implementation. tit.sim.mTI : N-pair mTI simulation implementation. tit.opt : Optimization modules that consume simulation results. tit.analyzer : Field analysis applied to simulation outputs.

BaseSimulation

BaseSimulation(config: SimulationConfig, montage: Montage, logger)

Bases: ABC

Abstract base class for TI/mTI simulations.

Provides the template-method run pipeline that subclasses customise by implementing _build_session and _post_process.

Parameters

config : SimulationConfig Fully specified simulation configuration. montage : Montage The electrode montage to simulate. logger : logging.Logger Logger instance for status and diagnostic messages.

Attributes

config : SimulationConfig Simulation configuration supplied at construction. montage : Montage Electrode montage supplied at construction. logger : logging.Logger Logger instance used throughout the pipeline. pm : tit.paths.PathManager Singleton path manager for BIDS path resolution. m2m_dir : str Absolute path to the subject's m2m_<subject> directory.

See Also

TISimulation : Concrete 2-pair TI subclass. mTISimulation : Concrete N-pair mTI subclass. run_simulation : Orchestrates BaseSimulation.run across montages.

Source code in tit/sim/base.py
def __init__(self, config: SimulationConfig, montage: Montage, logger):
    self.config = config
    self.montage = montage
    self.logger = logger
    self.pm = get_path_manager()
    self.m2m_dir = self.pm.m2m(config.subject_id)

run

run(simulation_dir: str) -> dict

Execute the full simulation pipeline for one montage.

This template method orchestrates directory setup, montage visualisation, SimNIBS FEM execution, and subclass-specific post-processing.

Parameters

simulation_dir : str Root simulations directory for the subject.

Returns

dict Result dictionary with keys montage_name, montage_type, status, and output_mesh.

See Also

run_simulation : Calls run for each montage in a config.

Source code in tit/sim/base.py
def run(self, simulation_dir: str) -> dict:
    """Execute the full simulation pipeline for one montage.

    This template method orchestrates directory setup, montage
    visualisation, SimNIBS FEM execution, and subclass-specific
    post-processing.

    Parameters
    ----------
    simulation_dir : str
        Root simulations directory for the subject.

    Returns
    -------
    dict
        Result dictionary with keys ``montage_name``, ``montage_type``,
        ``status``, and ``output_mesh``.

    See Also
    --------
    run_simulation : Calls ``run`` for each montage in a config.
    """
    montage_dir = self.pm.simulation(
        self.config.subject_id,
        self.montage.name,
    )
    dirs = setup_montage_directories(montage_dir, self._simulation_mode)
    create_simulation_config_file(
        self.config, self.montage, dirs["documentation"], self.logger
    )

    viz_pairs = None if self.montage.is_xyz else self.montage.electrode_pairs

    run_montage_visualization(
        montage_name=self.montage.name,
        simulation_mode=self._simulation_mode,
        eeg_net=self.montage.eeg_net,
        output_dir=dirs[self._montage_imgs_key],
        logger=self.logger,
        electrode_pairs=viz_pairs,
    )

    self.logger.info("SimNIBS simulation: Started")
    run_simnibs(self._build_session(dirs["hf_dir"]))
    self.logger.info("SimNIBS simulation: \u2713 Complete")

    output_mesh = self._post_process(dirs)
    self.logger.info(f"\u2713 {self.montage.name} complete")

    return {
        "montage_name": self.montage.name,
        "montage_type": self._montage_type_label,
        "status": "completed",
        "output_mesh": output_mesh,
    }

SimulationConfig dataclass

SimulationConfig(subject_id: str, montages: list[Montage], conductivity: str = 'scalar', intensities: list[float] = (lambda: [1.0, 1.0])(), electrode_shape: str = 'ellipse', electrode_dimensions: list[float] = (lambda: [8.0, 8.0])(), gel_thickness: float = 4.0, rubber_thickness: float = 2.0, map_to_surf: bool = True, map_to_vol: bool = False, map_to_mni: bool = False, map_to_fsavg: bool = False, open_in_gmsh: bool = False, tissues_in_niftis: str = 'all', aniso_maxratio: float = 10.0, aniso_maxcond: float = 2.0)

Full configuration for a TI or mTI simulation run.

Passed to :func:tit.sim.run_simulation to execute one or more montage simulations for a single subject. Electrode geometry, conductivity model, and output mapping options are all set here.

Attributes

subject_id : str Subject identifier (e.g. "sub-001"). montages : list[Montage] One or more :class:Montage definitions to simulate. conductivity : str Tissue conductivity model. One of:

- ``"scalar"`` -- isotropic scalar conductivities (default).
- ``"vn"`` -- volume-normalized anisotropic conductivities.
- ``"dir"`` -- directly-mapped anisotropic conductivities.
- ``"mc"`` -- mean-conductivity anisotropic conductivities.

The anisotropic modes (``"vn"``, ``"dir"``, ``"mc"``) require
DTI tensors registered to the head mesh.

intensities : list[float] Per-pair current intensities in mA. Length must be 1 (broadcast to all pairs) or match the total number of electrode pairs. Defaults to [1.0, 1.0]. electrode_shape : str Electrode shape ("ellipse" or "rect"). electrode_dimensions : list[float] [width, height] of each electrode in mm. gel_thickness : float Conductive-gel layer thickness in mm. rubber_thickness : float Rubber (silicone) layer thickness in mm. map_to_surf : bool Map results onto the cortical surface. Must be True because TI_normal calculation requires surface overlays. map_to_vol : bool Reserved for NIfTI output (handled externally by tit.tools.mesh2nii, not by SimNIBS SESSION). map_to_mni : bool Reserved; not currently passed to SimNIBS. map_to_fsavg : bool Reserved; not currently passed to SimNIBS. open_in_gmsh : bool Open results in Gmsh after simulation. tissues_in_niftis : str Tissue selection for NIfTI export ("all" or a comma-separated list). aniso_maxratio : float Maximum eigenvalue ratio clamp for anisotropic conductivity tensors. aniso_maxcond : float Maximum absolute conductivity clamp (S/m) for anisotropic tensors.

Raises

ValueError If conductivity is not one of the valid model names.

See Also

Montage : Electrode montage contained in montages. run_simulation : Entry point that consumes this config. parse_intensities : Helper to build the intensities list.

Montage dataclass

Montage(name: str, mode: MontageMode, electrode_pairs: list[tuple], eeg_net: str | None = None)

A named electrode montage used in a TI/mTI simulation.

Wraps the electrode pair definitions for a single montage. Electrodes may be referenced by EEG-cap label names (NET / FLEX_MAPPED modes) or by 3-D XYZ coordinates (FLEX_FREE / FREEHAND modes).

The simulation type is auto-detected from the number of electrode pairs: 2 pairs = standard TI, 4+ pairs = multi-channel mTI.

Attributes

name : str Human-readable montage name (e.g. "M1_left"). mode : MontageMode How electrode positions are specified. See :class:MontageMode. electrode_pairs : list[tuple] List of electrode pairs. Each element is a tuple of two electrode identifiers (label strings or XYZ coordinate lists). eeg_net : str or None Filename of the EEG-net CSV (e.g. "GSN-HydroCel-185.csv"). Required for NET and FLEX_MAPPED modes, ignored otherwise.

See Also

SimulationConfig : Holds one or more Montage instances. load_montages : Build Montage objects from montage_list.json.

is_xyz property

is_xyz: bool

Whether electrodes are specified as 3-D XYZ coordinates.

Returns

bool True for FLEX_FREE and FREEHAND modes.

simulation_mode property

simulation_mode: SimulationMode

Infer TI vs mTI from the number of electrode pairs.

Returns

SimulationMode SimulationMode.TI for 2 pairs, SimulationMode.MTI for 4 or more pairs.

Raises

ValueError If the pair count is not 2 or >= 4.

See Also

SimulationMode : The returned enum type.

num_pairs property

num_pairs: int

Number of electrode pairs in this montage.

Returns

int Length of :attr:electrode_pairs.

SimulationMode

Bases: Enum

Simulation type: standard two-pair TI or multi-channel mTI.

Attributes

TI : str Standard 2-pair temporal interference. MTI : str Multi-channel temporal interference (4+ pairs).

See Also

Montage.simulation_mode : Auto-detects mode from pair count.

parse_intensities

parse_intensities(s: str) -> list[float]

Parse a comma-separated intensity string into a list of floats.

A single value is duplicated to form a pair ("2.0" becomes [2.0, 2.0]). Otherwise the value count must be even so that each electrode pair receives two intensities.

Parameters

s : str Comma-separated intensity values (e.g. "1.0,2.0").

Returns

list[float] List of floats with an even number of elements.

Raises

ValueError If the number of values is odd and greater than 1.

See Also

SimulationConfig.intensities : Field populated by this function.

Source code in tit/sim/config.py
def parse_intensities(s: str) -> list[float]:
    """Parse a comma-separated intensity string into a list of floats.

    A single value is duplicated to form a pair (``"2.0"`` becomes
    ``[2.0, 2.0]``).  Otherwise the value count must be even so that
    each electrode pair receives two intensities.

    Parameters
    ----------
    s : str
        Comma-separated intensity values (e.g. ``"1.0,2.0"``).

    Returns
    -------
    list[float]
        List of floats with an even number of elements.

    Raises
    ------
    ValueError
        If the number of values is odd and greater than 1.

    See Also
    --------
    SimulationConfig.intensities : Field populated by this function.
    """
    v = [float(x.strip()) for x in s.split(",")]
    n = len(v)
    if n == 1:
        return [v[0], v[0]]
    if n >= 2 and n % 2 == 0:
        return v
    raise ValueError(
        f"Invalid intensity format: expected 1 or an even number of values; got {n}: {s!r}"
    )

run_simulation

run_simulation(config: SimulationConfig, logger=None, progress_callback: Callable[[int, int, str], None] | None = None) -> list[dict]

Run TI or mTI simulations for every montage in config.

For each montage in config.montages, this function:

  1. Auto-detects TI (2 pairs) vs mTI (4+ pairs) from the montage.
  2. Builds a SimNIBS SESSION with electrode geometry and conductivity settings from config.
  3. Runs the FEM solver to compute electric-field distributions.
  4. Computes temporal-interference envelope fields (TI_max, TI_normal) and, for mTI, the multi-channel superposition.
  5. Writes output meshes, surface overlays, and NIfTIs to the BIDS-compliant simulation directory.

Montages are processed sequentially. If no logger is provided, a file logger is created under the subject's log directory.

Parameters

config : SimulationConfig Full simulation configuration including subject ID, montage list, electrode geometry, and conductivity model. logger : logging.Logger or None, optional Logger instance. If None, a file logger is created automatically in the subject's BIDS logs directory. progress_callback : callable or None, optional Optional callback invoked before each montage as callback(current_index, total, montage_name) and once more with (total, total, "Complete") when finished.

Returns

list[dict] One result dict per montage with keys montage_name, montage_type, status, and output_mesh.

See Also

SimulationConfig : The configuration consumed by this function. BaseSimulation.run : Per-montage pipeline called internally. TISimulation : Concrete class for 2-pair simulations. mTISimulation : Concrete class for N-pair simulations.

Source code in tit/sim/utils.py
def run_simulation(
    config: SimulationConfig,
    logger=None,
    progress_callback: Callable[[int, int, str], None] | None = None,
) -> list[dict]:
    """Run TI or mTI simulations for every montage in *config*.

    For each montage in ``config.montages``, this function:

    1. Auto-detects TI (2 pairs) vs mTI (4+ pairs) from the montage.
    2. Builds a SimNIBS SESSION with electrode geometry and conductivity
       settings from *config*.
    3. Runs the FEM solver to compute electric-field distributions.
    4. Computes temporal-interference envelope fields (``TI_max``,
       ``TI_normal``) and, for mTI, the multi-channel superposition.
    5. Writes output meshes, surface overlays, and NIfTIs to the
       BIDS-compliant simulation directory.

    Montages are processed sequentially.  If no *logger* is provided,
    a file logger is created under the subject's log directory.

    Parameters
    ----------
    config : SimulationConfig
        Full simulation configuration including subject ID, montage
        list, electrode geometry, and conductivity model.
    logger : logging.Logger or None, optional
        Logger instance.  If ``None``, a file logger is created
        automatically in the subject's BIDS logs directory.
    progress_callback : callable or None, optional
        Optional callback invoked before each montage as
        ``callback(current_index, total, montage_name)`` and once more
        with ``(total, total, "Complete")`` when finished.

    Returns
    -------
    list[dict]
        One result dict per montage with keys ``montage_name``,
        ``montage_type``, ``status``, and ``output_mesh``.

    See Also
    --------
    SimulationConfig : The configuration consumed by this function.
    BaseSimulation.run : Per-montage pipeline called internally.
    TISimulation : Concrete class for 2-pair simulations.
    mTISimulation : Concrete class for N-pair simulations.
    """
    # Determine dominant simulation type for telemetry
    from tit.telemetry import track_operation

    has_mti = any(m.simulation_mode == SimulationMode.MTI for m in config.montages)
    _tel_op = const.TELEMETRY_OP_SIM_MTI if has_mti else const.TELEMETRY_OP_SIM_TI

    with track_operation(_tel_op):
        return _run_simulation_inner(config, logger, progress_callback)

load_montages

load_montages(montage_names: list[str], eeg_net: str, include_flex: bool = True) -> list[Montage]

Load named montages from the project's montage_list.json.

Reads the montage_list.json file (managed by :func:ensure_montage_file), looks up each name under the given EEG net's uni- and multi-polar sections, and returns them as :class:Montage instances. When include_flex is True, any flex/freehand montages found via :func:load_flex_montages are appended.

The eeg_net value determines the montage mode:

  • "freehand" sets Montage.Mode.FREEHAND
  • "flex_mode" sets Montage.Mode.FLEX_FREE
  • Any other value (e.g. "GSN-HydroCel-185.csv") sets Montage.Mode.NET
Parameters

montage_names : list[str] Names to look up in the montage file. eeg_net : str EEG net identifier that selects the sub-dict inside montage_list.json["nets"]. include_flex : bool, optional If True (default), append flex/freehand montages loaded from the FLEX_MONTAGES_FILE environment variable.

Returns

list[Montage] Resolved montage objects ready for simulation.

See Also

list_montage_names : Discover available names before loading. upsert_montage : Add montages that can then be loaded. Montage : The returned dataclass type.

Source code in tit/sim/utils.py
def load_montages(
    montage_names: list[str],
    eeg_net: str,
    include_flex: bool = True,
) -> list[Montage]:
    """Load named montages from the project's ``montage_list.json``.

    Reads the ``montage_list.json`` file (managed by
    :func:`ensure_montage_file`), looks up each name under the given
    EEG net's uni- and multi-polar sections, and returns them as
    :class:`Montage` instances.  When *include_flex* is ``True``, any
    flex/freehand montages found via :func:`load_flex_montages` are
    appended.

    The *eeg_net* value determines the montage mode:

    * ``"freehand"`` sets ``Montage.Mode.FREEHAND``
    * ``"flex_mode"`` sets ``Montage.Mode.FLEX_FREE``
    * Any other value (e.g. ``"GSN-HydroCel-185.csv"``) sets
      ``Montage.Mode.NET``

    Parameters
    ----------
    montage_names : list[str]
        Names to look up in the montage file.
    eeg_net : str
        EEG net identifier that selects the sub-dict inside
        ``montage_list.json["nets"]``.
    include_flex : bool, optional
        If ``True`` (default), append flex/freehand montages loaded
        from the ``FLEX_MONTAGES_FILE`` environment variable.

    Returns
    -------
    list[Montage]
        Resolved montage objects ready for simulation.

    See Also
    --------
    list_montage_names : Discover available names before loading.
    upsert_montage : Add montages that can then be loaded.
    Montage : The returned dataclass type.
    """
    data = load_montage_data()
    net = data.get("nets", {}).get(eeg_net, {})
    uni = net.get("uni_polar_montages", {})
    multi = net.get("multi_polar_montages", {})

    if eeg_net == "freehand":
        mode = Montage.Mode.FREEHAND
    elif eeg_net == "flex_mode":
        mode = Montage.Mode.FLEX_FREE
    else:
        mode = Montage.Mode.NET

    montages = [
        Montage(
            name=n,
            mode=mode,
            electrode_pairs=multi.get(n) or uni[n],
            eeg_net=eeg_net,
        )
        for n in montage_names
        if n in multi or n in uni
    ]

    if include_flex:
        for flex in load_flex_montages():
            montages.append(parse_flex_montage(flex))

    return montages

list_montage_names

list_montage_names(eeg_net: str, *, mode: str) -> list[str]

List all montage names defined under an EEG net.

Parameters

eeg_net : str EEG net identifier (e.g. "GSN-HydroCel-185.csv"). mode : str "U" for uni-polar montage names or "M" for multi-polar montage names.

Returns

list[str] Sorted montage names. Returns an empty list if the net or mode key does not exist.

See Also

upsert_montage : Add montage names to the list. load_montages : Load the named montages as Montage objects.

Source code in tit/sim/utils.py
def list_montage_names(eeg_net: str, *, mode: str) -> list[str]:
    """List all montage names defined under an EEG net.

    Parameters
    ----------
    eeg_net : str
        EEG net identifier (e.g. ``"GSN-HydroCel-185.csv"``).
    mode : str
        ``"U"`` for uni-polar montage names or ``"M"`` for
        multi-polar montage names.

    Returns
    -------
    list[str]
        Sorted montage names.  Returns an empty list if the net or
        mode key does not exist.

    See Also
    --------
    upsert_montage : Add montage names to the list.
    load_montages : Load the named montages as ``Montage`` objects.
    """
    data = load_montage_data()
    net = data.get("nets", {}).get(eeg_net, {})
    key = "uni_polar_montages" if mode.upper() == "U" else "multi_polar_montages"
    return sorted(net.get(key, {}).keys())

load_montage_data

load_montage_data() -> dict

Load the full montage_list.json as a dict.

Returns

dict Parsed JSON with top-level key "nets" mapping EEG net names to their uni/multi polar montage definitions.

See Also

save_montage_data : Write the dict back to disk. ensure_montage_file : Guarantees the file exists before reading.

Source code in tit/sim/utils.py
def load_montage_data() -> dict:
    """Load the full ``montage_list.json`` as a dict.

    Returns
    -------
    dict
        Parsed JSON with top-level key ``"nets"`` mapping EEG net
        names to their uni/multi polar montage definitions.

    See Also
    --------
    save_montage_data : Write the dict back to disk.
    ensure_montage_file : Guarantees the file exists before reading.
    """
    with open(ensure_montage_file()) as f:
        return json.load(f)

save_montage_data

save_montage_data(data: dict) -> None

Write data to montage_list.json, overwriting the file.

Parameters

data : dict Full montage dict (must contain a "nets" key).

See Also

load_montage_data : Read the data back after saving.

Source code in tit/sim/utils.py
def save_montage_data(data: dict) -> None:
    """Write *data* to ``montage_list.json``, overwriting the file.

    Parameters
    ----------
    data : dict
        Full montage dict (must contain a ``"nets"`` key).

    See Also
    --------
    load_montage_data : Read the data back after saving.
    """
    with open(ensure_montage_file(), "w") as f:
        json.dump(data, f, indent=4)

ensure_montage_file

ensure_montage_file() -> str

Return the path to montage_list.json, creating it if absent.

If the file does not exist, creates it with the default schema {"nets": {}}.

Returns

str Absolute path to the montage_list.json file.

See Also

load_montage_data : Read the file returned by this function. save_montage_data : Write data to the file returned by this function.

Source code in tit/sim/utils.py
def ensure_montage_file() -> str:
    """Return the path to ``montage_list.json``, creating it if absent.

    If the file does not exist, creates it with the default schema
    ``{"nets": {}}``.

    Returns
    -------
    str
        Absolute path to the ``montage_list.json`` file.

    See Also
    --------
    load_montage_data : Read the file returned by this function.
    save_montage_data : Write data to the file returned by this function.
    """
    path = _montage_list_path()
    os.makedirs(os.path.dirname(path), exist_ok=True)
    if not os.path.exists(path):
        with open(path, "w") as f:
            json.dump({"nets": {}}, f, indent=4)
    return path

upsert_montage

upsert_montage(*, eeg_net: str, montage_name: str, electrode_pairs: list[list[str]], mode: str) -> None

Insert or update a montage definition in montage_list.json.

Creates the EEG net entry if it does not already exist.

Parameters

eeg_net : str EEG net identifier (e.g. "GSN-HydroCel-185.csv"). montage_name : str Human-readable montage name. electrode_pairs : list[list[str]] List of electrode pairs, each a two-element list of electrode labels (e.g. [["E1", "E2"], ["E3", "E4"]]). mode : str "U" for uni-polar montages (2-pair TI) or "M" for multi-polar montages (4-pair mTI).

See Also

list_montage_names : List montage names after upserting. load_montages : Load upserted montages as Montage objects.

Source code in tit/sim/utils.py
def upsert_montage(
    *,
    eeg_net: str,
    montage_name: str,
    electrode_pairs: list[list[str]],
    mode: str,
) -> None:
    """Insert or update a montage definition in ``montage_list.json``.

    Creates the EEG net entry if it does not already exist.

    Parameters
    ----------
    eeg_net : str
        EEG net identifier (e.g. ``"GSN-HydroCel-185.csv"``).
    montage_name : str
        Human-readable montage name.
    electrode_pairs : list[list[str]]
        List of electrode pairs, each a two-element list of electrode
        labels (e.g. ``[["E1", "E2"], ["E3", "E4"]]``).
    mode : str
        ``"U"`` for uni-polar montages (2-pair TI) or ``"M"`` for
        multi-polar montages (4-pair mTI).

    See Also
    --------
    list_montage_names : List montage names after upserting.
    load_montages : Load upserted montages as ``Montage`` objects.
    """
    data = load_montage_data()
    net = data["nets"].setdefault(
        eeg_net, {"uni_polar_montages": {}, "multi_polar_montages": {}}
    )
    key = "uni_polar_montages" if mode.upper() == "U" else "multi_polar_montages"
    net[key][montage_name] = electrode_pairs
    save_montage_data(data)