Skip to content

Simulation

The simulation engine runs finite element method (FEM) calculations using SimNIBS to compute temporal interference electric fields in the brain. TI-Toolbox supports two simulation modes: TI (2-pair) and mTI (4+ pairs, even).

graph LR
    MESH([Head Mesh]) --> SIM[SimNIBS FEM]
    MONT([Montage Config]) --> SIM
    INT([Intensity Config]) --> SIM
    SIM --> TI_MAX([TI_max Field])
    SIM --> TI_NORM([TI_normal Field])
    SIM --> NIFTI([NIfTI Volumes])
    style MESH fill:#1a3a5c,stroke:#48a,color:#fff
    style MONT fill:#1a3a5c,stroke:#48a,color:#fff
    style INT fill:#1a3a5c,stroke:#48a,color:#fff
    style SIM fill:#2d5a27,stroke:#4a8,color:#fff
    style TI_MAX fill:#1a5c4a,stroke:#4a8,color:#fff
    style TI_NORM fill:#1a5c4a,stroke:#4a8,color:#fff
    style NIFTI fill:#1a5c4a,stroke:#4a8,color:#fff

Running a Simulation

from tit.sim import (
    SimulationConfig, Montage,
    run_simulation, load_montages,
)

# Load montages from the project's montage_list.json
montages = load_montages(
    montage_names=["motor_cortex"],
    eeg_net="GSN-HydroCel-185",
)

# Configure the simulation (montages are part of the config)
config = SimulationConfig(
    subject_id="001",
    montages=montages,
    conductivity="scalar",
    intensities=[1.0, 1.0],
    electrode_shape="ellipse",
    electrode_dimensions=[8.0, 8.0],
    gel_thickness=4.0,
)

# Run (auto-detects TI vs mTI based on number of electrode pairs)
results = run_simulation(config)

Simulation Modes

The simulation mode is auto-detected from the montage configuration:

Mode Electrode Pairs Description
TI 2 pairs Standard temporal interference — two pairs of electrodes at slightly different frequencies
mTI 4+ pairs (even) Multi-channel TI — N electrode pairs combined via binary-tree algorithm

Auto-Detection

You do not need to specify the mode manually. TI-Toolbox inspects the montage: 2 pairs triggers TI mode, 4 or more pairs (even) triggers mTI mode.

Montage Configuration

Montages define which electrodes form each pair. They are stored in montage_list.json within your project:

{
  "nets": {
    "GSN-HydroCel-185": {
      "uni_polar_montages": {
        "motor_cortex": [["E36", "E224"], ["E104", "E148"]]
      }
    }
  }
}

Load montages programmatically:

from tit.sim import load_montages

montages = load_montages(
    montage_names=["motor_cortex", "frontal_target"],
    eeg_net="GSN-HydroCel-185",
)

Conductivity Types

The conductivity field on SimulationConfig accepts one of four string values:

Value Description
"scalar" Isotropic, piecewise-constant conductivity (default, faster)
"vn" Volume-normalized anisotropic conductivity from DTI data
"dir" Direct linear rescaling of diffusion tensor eigenvalues
"mc" Mean-conductivity (isotropic but spatially varying, from DTI)

Electrode Configuration

Electrode parameters are flat fields on SimulationConfig:

config = SimulationConfig(
    ...,
    electrode_shape="ellipse",         # "ellipse" or "rect"
    electrode_dimensions=[8.0, 8.0],   # [width, height] in mm
    gel_thickness=4.0,                  # gel layer thickness in mm
    rubber_thickness=2.0,              # rubber layer thickness in mm
)

Output Fields

After simulation, TI-Toolbox produces several derived field types:

Field Description
TI_max Maximum TI envelope magnitude — scalar field on volume elements (TI mode)
TI_normal TI field component normal to cortical surface — node field on surface overlay (TI mode)
TI_Max Maximum mTI envelope magnitude — scalar field on volume elements (mTI mode)
TI_vectors Intermediate pairwise TI vector fields for each adjacent pair (mTI mode)

Outputs are saved as both mesh files (for surface analysis) and NIfTI volumes (for voxel analysis).

Output Directory Structure

derivatives/SimNIBS/sub-001/Simulations/
└── motor_cortex/
    ├── high_Frequency/
    │   ├── mesh/             # Per-pair HF mesh files
    │   ├── niftis/           # Per-pair HF NIfTI volumes
    │   └── analysis/         # fields_summary.txt
    ├── TI/
    │   ├── mesh/             # TI_max and GM/WM mesh files
    │   ├── niftis/           # TI NIfTI volumes (subject + MNI space)
    │   ├── surface_overlays/ # Cortical surface overlays (for TI_normal)
    │   └── montage_imgs/     # Electrode placement visualizations
    ├── mTI/                  # (mTI mode only)
    │   ├── mesh/             # mTI_max and intermediate TI meshes
    │   ├── niftis/           # mTI NIfTI volumes
    │   └── montage_imgs/     # Electrode placement visualizations
    └── documentation/        # config.json, SimNIBS logs

API Reference

tit.sim.config.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.

tit.sim.config.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.

tit.sim.config.MontageMode

Bases: Enum

How electrode positions are specified.

NET and FLEX_MAPPED use EEG-cap label names resolved against an EEG-net CSV. FLEX_FREE and FREEHAND use raw 3-D XYZ coordinates (no net required).

Attributes

NET : str Standard EEG-cap electrode labels. FLEX_MAPPED : str Flex-search result mapped back to EEG-cap labels. FLEX_FREE : str Flex-search result with free XYZ coordinates. FREEHAND : str User-specified XYZ coordinates (manual placement).

tit.sim.config.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.

tit.sim.config.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}"
    )

tit.sim.base.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,
    }

tit.sim.utils.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)

tit.sim.utils.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

tit.sim.utils.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())

tit.sim.utils.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)

tit.sim.utils.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)

tit.sim.utils.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

tit.sim.utils.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)