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
run ¶
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
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.
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 ¶
parse_intensities ¶
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
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:
- Auto-detects TI (2 pairs) vs mTI (4+ pairs) from the montage.
- Builds a SimNIBS SESSION with electrode geometry and conductivity settings from config.
- Runs the FEM solver to compute electric-field distributions.
- Computes temporal-interference envelope fields:
TI_max/TI_avgplusTI_normal(2-pair TI only), or the multi-channel superposition for mTI. - 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
960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 | |
load_montages ¶
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"setsMontage.Mode.FREEHAND"flex_mode"setsMontage.Mode.FLEX_FREE- Any other value (e.g.
"GSN-HydroCel-185.csv") setsMontage.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
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 | |
list_montage_names ¶
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
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
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
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
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.