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. MontageMode Enum saying how a montage's electrodes are specified (net labels or XYZ coordinates); also reachable as Montage.Mode. 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 the named montages (only those, in that order) from the project's montage_list.json. list_montage_names List the montage names available under an EEG net -- call this first to see what load_montages can return. 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.

Examples

from tit.sim import SimulationConfig, run_simulation, load_montages, list_montage_names list_montage_names("GSN-HydroCel-185.csv", mode="U") # doctest: +SKIP ['L_Insula', 'R_Insula'] montages = load_montages(["L_Insula"], eeg_net="GSN-HydroCel-185.csv") # doctest: +SKIP cfg = SimulationConfig(subject_id="ernie", montages=montages) # doctest: +SKIP run_simulation(cfg) # doctest: +SKIP

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, *, overwrite: bool = False) -> 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, *, overwrite: bool = False) -> 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")
    session = self._build_session(dirs["hf_dir"])
    if overwrite:
        # run_simnibs does not expose this supported SESSION option. Keep the
        # native existence guard for ordinary runs; never delete marker files.
        session.run(allow_multiple_runs=True)
    else:
        run_simnibs(session)
    self.logger.info("SimNIBS simulation: \u2713 Complete")

    output_mesh = self._post_process(dirs)
    self._write_report()
    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,
        "output_dir": montage_dir,
    }

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 = True, open_in_gmsh: bool = False, tissues_in_niftis: str = 'all', aniso_maxratio: float = 10.0, aniso_maxcond: float = 2.0, output_fields: list[str] = (lambda: [FIELD_TI_MAX])(), tissue_conductivities: dict[int, float] | None = None)

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 without the sub- prefix (e.g. "ernie", "101"); must match an existing m2m_<subject_id> directory. montages : list[Montage] One or more :class:Montage definitions to simulate. Two pairs per montage is TI, four or more (even) pairs is mTI -- detected per montage, so a list may mix both. 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] Current per electrode pair in mA, in electrode_pairs order. A TI montage reads the first two values; an mTI montage needs one value per pair (len(intensities) >= num_pairs). A single value is not broadcast -- a 4-pair montage with the default two values is rejected. Default [1.0, 1.0]. electrode_shape : str Electrode shape, "ellipse" or "rect". Default "ellipse". electrode_dimensions : list[float] [width, height] of each electrode in mm. Default [8.0, 8.0]. gel_thickness : float Conductive-gel layer thickness in mm. Default 4.0. rubber_thickness : float Rubber (silicone) layer thickness in mm. Default 2.0. map_to_surf : bool Map results onto the cortical surface. Must stay True (default) because the TI_normal calculation requires surface overlays. map_to_vol : bool Reserved for NIfTI output (handled externally by tit.tools.mesh2nii, not by SimNIBS SESSION). Default False. map_to_mni : bool Generate MNI-space field and T1 NIfTI outputs after simulation. Default False; subject-space NIfTI outputs are always generated. map_to_fsavg : bool After each TI montage finishes, project its surface fields (TI_max, TI_normal, hf_peak, hf_sar) onto fsaverage5 for group surface analysis. Default True; set False to skip. Failures are logged and never abort the simulation. open_in_gmsh : bool Open results in Gmsh after simulation. Default False. tissues_in_niftis : str Tissue selection for NIfTI export ("all" or a comma-separated list of tissue numbers). Default "all". aniso_maxratio : float Maximum eigenvalue ratio clamp for anisotropic conductivity tensors. Default 10.0. aniso_maxcond : float Maximum absolute conductivity clamp (S/m) for anisotropic tensors. Default 2.0. output_fields : list[str] Which volume-mesh fields to compute and write. Logical names from :data:tit.constants.SELECTABLE_OUTPUT_FIELDS: "TI_max", "TI_avg", "hf_peak", "hf_sar". "TI_max" maps to mTI_max on disk for mTI meshes. Defaults to ["TI_max"] only -- TI_avg and the safety fields (hf_peak, hf_sar) must be opted into. tissue_conductivities : dict[int, float] or None Per-tissue conductivity overrides (S/m), keyed by SimNIBS tissue number (1-based, matching tdcs.cond index + 1). None (the default) uses SimNIBS's own tissue defaults for every tissue. JSON object keys are always strings, so this round-trips as {"<tissue number>": <S/m>} on disk; :func:tit.config_io.deserialize_config coerces the keys back to int, and this dataclass's own __post_init__ does the same for an instance built directly with string keys. Consumed by tit.sim.__main__ via TISSUE_COND_<n> environment variables (the mechanism tit.sim.base.BaseSimulation._apply_tissue_conductivities already reads) -- deprecated in favor of this field, which is visible in the job's config and its manifest instead of being an invisible process-environment side channel.

Raises

ValueError If conductivity is not one of the valid model names, if output_fields contains an unknown name or is empty, if any montage has a pair that is not exactly two electrodes, if intensities is shorter than a montage needs (2 for TI, one per pair for mTI), or if tissue_conductivities contains a non-positive value. Filesystem checks (the m2m directory, the EEG-net CSV) happen later, in :func:run_simulation.

Examples

from tit.sim import SimulationConfig, Montage, MontageMode montage = Montage( ... name="L_Insula", mode=MontageMode.NET, ... electrode_pairs=[("E010", "E011"), ("E012", "E013")], ... eeg_net="GSN-HydroCel-185.csv", ... ) cfg = SimulationConfig( ... subject_id="ernie", ... montages=[montage], ... conductivity="scalar", ... intensities=[1.0, 1.0], ... electrode_shape="ellipse", ... electrode_dimensions=[8.0, 8.0], ... output_fields=["TI_max", "hf_peak"], ... ) cfg.map_to_surf, cfg.output_fields (True, ['TI_max', 'hf_peak'])

Then run_simulation(cfg) (needs SimNIBS and the subject's m2m directory).

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[str | list[float], str | list[float]]], eeg_net: str | None = None, display_name: str | None = None, electrode_poses: list[list[list[float]]] | None = None, provenance: dict[str, 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. display_name : str or None Optional user-facing label. name remains the storage and lookup key. electrode_poses : list[list[list[float]]] or None Optional full 4x4 homogeneous pose (row-major) per electrode, one per XYZ position in electrode_pairs order. Only meaningful for XYZ modes; preserves rectangular-electrode orientation when a flex-search candidate is replayed. None (default) lets SimNIBS orient the electrodes itself. provenance : dict[str, str] or None Free-form origin metadata (e.g. {"head_mesh_sha256": ...}) checked by :func:run_simulation when present.

Raises

ValueError If electrode_poses is given for a label-based montage, does not contain exactly one 4x4 right-handed homogeneous transform per electrode, or its translations do not match electrode_pairs.

Examples

from tit.sim import Montage, MontageMode m = Montage( ... name="L_Insula", ... mode=MontageMode.NET, ... electrode_pairs=[("E010", "E011"), ("E012", "E013")], ... eeg_net="GSN-HydroCel-185.csv", ... ) m.num_pairs, m.simulation_mode.value, m.is_xyz (2, 'TI', False)

A free-hand montage uses subject-space millimetre coordinates and no net:

free = Montage( ... name="custom", ... mode=MontageMode.FREEHAND, ... electrode_pairs=[([-60.0, 10.0, 40.0], [60.0, 10.0, 40.0]), ... ([-60.0, -40.0, 40.0], [60.0, -40.0, 40.0])], ... ) free.is_xyz True

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 (even) pairs.

Raises

ValueError If the pair count is not allowed by :func:tit.constants.is_valid_pair_count -- an even count of at least 2. One pair is tACS, not TI; an odd count leaves a channel with nothing to beat against.

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.

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).

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: Logger | None = None, progress_callback: Callable[[int, int, str], None] | None = None, *, overwrite: bool = False) -> 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_avg plus TI_normal (2-pair TI only), or the multi-channel superposition for mTI.
  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.

bool, optional

Explicitly allow SimNIBS to rerun in the montage output directory. Defaults to False; existing-result protection stays enabled unless confirmed.

Returns

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

Raises

ValueError If config.montages is empty, the subject's m2m_<id> directory does not exist, a montage has an invalid pair count (must be an even number >= 2), too few intensities are given for a montage, a label-based montage has no eeg_net or its EEG-net CSV is missing from the subject's eeg_positions folder, or an XYZ montage holds a malformed coordinate. OSError Raised by SimNIBS when a montage output directory already holds results and overwrite is False.

Examples

from tit.sim import SimulationConfig, load_montages, run_simulation montages = load_montages(["L_Insula"], eeg_net="GSN-HydroCel-185.csv") # doctest: +SKIP cfg = SimulationConfig(subject_id="ernie", montages=montages, ... intensities=[1.0, 1.0], output_fields=["TI_max"]) # doctest: +SKIP results = run_simulation(cfg) # doctest: +SKIP results[0]["status"], results[0]["output_mesh"] # doctest: +SKIP ('completed', '.../Simulations/L_Insula/TI/mesh/ernie_L_Insula_TI.msh')

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: logging.Logger | None = None,
    progress_callback: Callable[[int, int, str], None] | None = None,
    *,
    overwrite: bool = False,
) -> 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_avg``
       plus ``TI_normal`` (2-pair TI only), or the multi-channel
       superposition for mTI.
    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.

    overwrite : bool, optional
        Explicitly allow SimNIBS to rerun in the montage output directory. Defaults
        to False; existing-result protection stays enabled unless confirmed.

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

    Raises
    ------
    ValueError
        If ``config.montages`` is empty, the subject's ``m2m_<id>``
        directory does not exist, a montage has an invalid pair count
        (must be an even number >= 2), too few *intensities* are given
        for a montage, a label-based montage has no *eeg_net* or its
        EEG-net CSV is missing from the subject's ``eeg_positions``
        folder, or an XYZ montage holds a malformed coordinate.
    OSError
        Raised by SimNIBS when a montage output directory already holds
        results and *overwrite* is ``False``.

    Examples
    --------
    >>> from tit.sim import SimulationConfig, load_montages, run_simulation
    >>> montages = load_montages(["L_Insula"], eeg_net="GSN-HydroCel-185.csv")  # doctest: +SKIP
    >>> cfg = SimulationConfig(subject_id="ernie", montages=montages,
    ...                        intensities=[1.0, 1.0], output_fields=["TI_max"])  # doctest: +SKIP
    >>> results = run_simulation(cfg)  # doctest: +SKIP
    >>> results[0]["status"], results[0]["output_mesh"]  # doctest: +SKIP
    ('completed', '.../Simulations/L_Insula/TI/mesh/ernie_L_Insula_TI.msh')

    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

    _validate_simulation_inputs(config)
    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, overwrite=overwrite
        )

load_montages

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

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

Only the montages named in montage_names are returned, in that order -- this is not a listing of every montage under the net. load_montages(["L_Insula"], ...) returns a one-element list, so indexing [1] raises IndexError. Use :func:list_montage_names to see which names exist. A name that is not defined under eeg_net is silently skipped (no error), so the result can be shorter than montage_names; check len() or the returned .name values before relying on positions.

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 and the FLEX_MONTAGES_FILE environment variable points at a file, any flex/freehand montages found via :func:load_flex_montages are appended after the named ones (nothing is appended when the variable is unset, the normal case for scripts).

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, e.g. ["L_Insula"]. Order is preserved in the result. eeg_net : str EEG-net CSV filename (e.g. "GSN-HydroCel-185.csv", "EEG10-10_UI_Jurak_2007.csv") that selects the sub-dict inside montage_list.json["nets"]; it is also stored on each returned :class:Montage as eeg_net. include_flex : bool, optional If True (default), append flex/freehand montages loaded from the FLEX_MONTAGES_FILE environment variable.

Returns

list[Montage] One :class:Montage per found name, in montage_names order, ready to pass as SimulationConfig(montages=...).

Examples

from tit.sim import list_montage_names, load_montages list_montage_names("GSN-HydroCel-185.csv", mode="U") # doctest: +SKIP ['L_Insula', 'R_Insula'] montages = load_montages(["L_Insula"], eeg_net="GSN-HydroCel-185.csv") # doctest: +SKIP len(montages), montages[0].name # doctest: +SKIP (1, 'L_Insula') both = load_montages(["L_Insula", "R_Insula"], eeg_net="GSN-HydroCel-185.csv") # doctest: +SKIP [m.name for m in both] # doctest: +SKIP ['L_Insula', 'R_Insula']

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 the named montages from the project's ``montage_list.json``.

    **Only the montages named in** *montage_names* **are returned, in that
    order** -- this is not a listing of every montage under the net.
    ``load_montages(["L_Insula"], ...)`` returns a one-element list, so
    indexing ``[1]`` raises ``IndexError``.  Use
    :func:`list_montage_names` to see which names exist.  A name that is
    not defined under *eeg_net* is silently skipped (no error), so the
    result can be shorter than *montage_names*; check ``len()`` or the
    returned ``.name`` values before relying on positions.

    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`` and the
    ``FLEX_MONTAGES_FILE`` environment variable points at a file, any
    flex/freehand montages found via :func:`load_flex_montages` are
    appended after the named ones (nothing is appended when the variable
    is unset, the normal case for scripts).

    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, e.g. ``["L_Insula"]``.
        Order is preserved in the result.
    eeg_net : str
        EEG-net CSV filename (e.g. ``"GSN-HydroCel-185.csv"``,
        ``"EEG10-10_UI_Jurak_2007.csv"``) that selects the sub-dict
        inside ``montage_list.json["nets"]``; it is also stored on each
        returned :class:`Montage` as ``eeg_net``.
    include_flex : bool, optional
        If ``True`` (default), append flex/freehand montages loaded
        from the ``FLEX_MONTAGES_FILE`` environment variable.

    Returns
    -------
    list[Montage]
        One :class:`Montage` per *found* name, in *montage_names* order,
        ready to pass as ``SimulationConfig(montages=...)``.

    Examples
    --------
    >>> from tit.sim import list_montage_names, load_montages
    >>> list_montage_names("GSN-HydroCel-185.csv", mode="U")  # doctest: +SKIP
    ['L_Insula', 'R_Insula']
    >>> montages = load_montages(["L_Insula"], eeg_net="GSN-HydroCel-185.csv")  # doctest: +SKIP
    >>> len(montages), montages[0].name  # doctest: +SKIP
    (1, 'L_Insula')
    >>> both = load_montages(["L_Insula", "R_Insula"], eeg_net="GSN-HydroCel-185.csv")  # doctest: +SKIP
    >>> [m.name for m in both]  # doctest: +SKIP
    ['L_Insula', 'R_Insula']

    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.

These are exactly the names :func:load_montages can resolve for that net; a name not in this list is silently skipped by :func:load_montages.

Parameters

eeg_net : str EEG-net CSV filename (e.g. "GSN-HydroCel-185.csv", "EEG10-10_UI_Jurak_2007.csv"), the key under montage_list.json["nets"]. mode : str "U" for uni-polar (2-pair TI) montage names or "M" for multi-polar (4+-pair mTI) montage names. Case-insensitive.

Returns

list[str] Sorted montage names. Returns an empty list if the net or mode key does not exist (never raises for an unknown net).

Examples

from tit.sim import list_montage_names list_montage_names("GSN-HydroCel-185.csv", mode="U") # doctest: +SKIP ['L_Insula', 'R_Insula'] list_montage_names("GSN-HydroCel-185.csv", mode="M") # doctest: +SKIP []

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.

    These are exactly the names :func:`load_montages` can resolve for
    that net; a name not in this list is silently skipped by
    :func:`load_montages`.

    Parameters
    ----------
    eeg_net : str
        EEG-net CSV filename (e.g. ``"GSN-HydroCel-185.csv"``,
        ``"EEG10-10_UI_Jurak_2007.csv"``), the key under
        ``montage_list.json["nets"]``.
    mode : str
        ``"U"`` for uni-polar (2-pair TI) montage names or ``"M"`` for
        multi-polar (4+-pair mTI) montage names.  Case-insensitive.

    Returns
    -------
    list[str]
        Sorted montage names.  Returns an empty list if the net or
        mode key does not exist (never raises for an unknown net).

    Examples
    --------
    >>> from tit.sim import list_montage_names
    >>> list_montage_names("GSN-HydroCel-185.csv", mode="U")  # doctest: +SKIP
    ['L_Insula', 'R_Insula']
    >>> list_montage_names("GSN-HydroCel-185.csv", mode="M")  # doctest: +SKIP
    []

    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(*, pm: PathManager | None = None) -> 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(*, pm: PathManager | None = None) -> 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(pm=pm)) as f:
        return json.load(f)

save_montage_data

save_montage_data(data: dict, *, pm: PathManager | None = None) -> 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, *, pm: PathManager | None = None) -> 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(pm=pm), "w") as f:
        json.dump(data, f, indent=4)

ensure_montage_file

ensure_montage_file(*, pm: PathManager | None = None) -> 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(*, pm: PathManager | None = None) -> 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(pm)
    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, pm: PathManager | None = None) -> 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 CSV filename exactly as it appears in the subject's eeg_positions folder, e.g. "GSN-HydroCel-185.csv" or "EEG10-10_UI_Jurak_2007.csv". Created in the file if absent. montage_name : str Montage name; an existing montage of the same name under this net and mode is replaced. electrode_pairs : list[list[str]] Electrode pairs, each a two-element list of labels from that net (e.g. [["E010", "E011"], ["E012", "E013"]]). Two pairs for mode="U", four (or more, even) for mode="M". mode : str "U" for uni-polar montages (2-pair TI) or "M" for multi-polar montages (4+-pair mTI). Case-insensitive; anything other than "U" is treated as "M". pm : PathManager or None, optional Path manager to locate the project; None uses the global one.

Returns

None The file is rewritten in place.

Examples

from tit.sim import upsert_montage, list_montage_names upsert_montage( ... eeg_net="GSN-HydroCel-185.csv", ... montage_name="L_Insula", ... electrode_pairs=[["E010", "E011"], ["E012", "E013"]], ... mode="U", ... ) # doctest: +SKIP list_montage_names("GSN-HydroCel-185.csv", mode="U") # doctest: +SKIP ['L_Insula']

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,
    pm: PathManager | None = None,
) -> 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 CSV filename exactly as it appears in the subject's
        ``eeg_positions`` folder, e.g. ``"GSN-HydroCel-185.csv"`` or
        ``"EEG10-10_UI_Jurak_2007.csv"``.  Created in the file if absent.
    montage_name : str
        Montage name; an existing montage of the same name under this net
        and mode is replaced.
    electrode_pairs : list[list[str]]
        Electrode pairs, each a two-element list of labels from that net
        (e.g. ``[["E010", "E011"], ["E012", "E013"]]``).  Two pairs for
        ``mode="U"``, four (or more, even) for ``mode="M"``.
    mode : str
        ``"U"`` for uni-polar montages (2-pair TI) or ``"M"`` for
        multi-polar montages (4+-pair mTI).  Case-insensitive; anything
        other than ``"U"`` is treated as ``"M"``.
    pm : PathManager or None, optional
        Path manager to locate the project; ``None`` uses the global one.

    Returns
    -------
    None
        The file is rewritten in place.

    Examples
    --------
    >>> from tit.sim import upsert_montage, list_montage_names
    >>> upsert_montage(
    ...     eeg_net="GSN-HydroCel-185.csv",
    ...     montage_name="L_Insula",
    ...     electrode_pairs=[["E010", "E011"], ["E012", "E013"]],
    ...     mode="U",
    ... )  # doctest: +SKIP
    >>> list_montage_names("GSN-HydroCel-185.csv", mode="U")  # doctest: +SKIP
    ['L_Insula']

    See Also
    --------
    list_montage_names : List montage names after upserting.
    load_montages : Load upserted montages as ``Montage`` objects.
    """
    data = load_montage_data(pm=pm)
    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, pm=pm)