Multipolar TI (mTI)
Multipolar temporal interference (mTI) generalizes standard 2-pair TI to four or more electrode pairs, driven by independent high-frequency carriers whose beat pattern is combined into one modulation envelope. This page covers the field math, how mTI is wired through the simulator and analyzer, and multipolar exhaustive search (mex-search). For montage sources, the simulator GUI, and standard TI post-processing, see Simulator; for 2-pair exhaustive search, see Ex-Search; for field analysis, see Analyzer.
Left column unipolar (two channels) right column multipolar (four channels). Panels A,D: target and electrode montage. Panels B,E: high frequency fields. Panels C,F: modulation fields.
What mTI Is, and How the Toolbox Detects It
TI vs. mTI is not a setting – it is inferred purely from how many electrode pairs a montage has. Montage.simulation_mode (tit/sim/config.py) does this:
- 2 pairs ->
SimulationMode.TI - 4 or more pairs ->
SimulationMode.MTI - 0, 1, or 3 pairs -> raises
ValueError("Invalid number of electrode pairs: {n}. Expected 2 (TI) or 4+ (mTI).")
There is no odd-pair-count mTI: an mTI montage must have an even number of pairs, and mTISimulation additionally caps it at 26 pairs (see Simulator Behavior for mTI below).
Montages are persisted in montage_list.json under separate keys per EEG net:
data["nets"][eeg_net]["multi_polar_montages"][name] = [[e1,e2],[e3,e4],[e5,e6],[e7,e8]]
data["nets"][eeg_net]["uni_polar_montages"][name] = [[e1,e2],[e3,e4]]
upsert_montage(..., mode="M") writes to multi_polar_montages; mode="U" writes to uni_polar_montages. When a montage is loaded by name, load_montages resolves it as multi.get(name) or uni[name] – if the same name exists in both dictionaries, the multi-polar entry wins.
A multipolar montage: 8 electrodes arranged in 4 channels (4 electrode pairs).
Field Math and Critical Values
The envelope for any number of coherent-beat electrode pairs reduces to two sufficient statistics of the fields’ projections onto a candidate direction n:
a_k = E_ka . n, b_k = E_kb . n (signed projections, pair k)
P = 0.5 * sum_k (a_k^2 + b_k^2) carrier power
Q = | sum_k a_k * b_k * exp(i*psi_k) | coherent phasor sum
MD = sqrt(2) * ( sqrt(P+Q) - sqrt(P-Q) ) modulation depth
psi_k is a per-pair envelope phase offset (radians), None by default (all pairs phase-aligned, psi_k=0, the standard case). P-Q and P+Q are clamped to >= 0 before the square roots to absorb floating-point round-off. This is implemented in tit/calc.py, whose public API is exactly five functions:
| Function | Purpose |
|---|---|
get_TI_vectors(E1, E2) |
Exact K=1 closed form (2 pairs) |
get_mTI_vectors(fields, channels=None, psi=None) |
K >= 1 envelope; the verified N>2 replacement |
get_TI_avg(fields, channels=None, psi=None) |
Direction-averaged modulation depth |
get_magnitude_am(fields) |
Direction-free AM envelope of ‖E(t)‖ (Botzanowski et al. 2025) |
get_nTI_vectors(fields) |
Deprecated. Delegates to get_mTI_vectors |
get_mTI_vectors is the function that mTI simulation and mex-search both call. It takes fields = [E_1a, E_1b, ..., E_Ka, E_Kb], 2K arrays of shape (N, 3), and returns (N, 3) modulation-amplitude vectors whose norm is MD.
- K=1 dispatches exactly to
get_TI_vectors, an exact closed form (Hirata et al. 2024, sign-agnostic): whenmin(|E1|,|E2|) <= sqrt(|E1.E2|),MD = 2*min(|E1|,|E2|)along the smaller field’s own (sign-corrected) direction; otherwiseMD = 2*|E1 x E2| / min(|E1-E2|, |E1+E2|), evaluated at the component of the smaller field perpendicular to whichever ofE1-E2/E1+E2has the smaller norm. No direction search is needed at K=1. - K>=2 has no closed form and is solved by a direction search: a coarse 192-point Fibonacci-sphere sweep (
num_directions=192), followed by 3 rounds of local patch refinement around up to 6 angularly-diverse coarse-sweep seeds (_REFINE_N_ROUNDS=3,_REFINE_N_SEEDS=6, minimum seed separation_REFINE_MIN_SEED_ANGLE_DEG=25.0, 16 points per patch, initial half-angle2.0/sqrt(192)radians shrinking by0.4each round). Elements are processed in chunks of16384to bound memory.get_TI_avgreuses the same coarse sweep but averages the envelope over all 192 sampled directions instead of taking the per-element argmax, and skips refinement (refinement only sharpens a single best direction, which an average does not need) – it is element-wise<= TI_max.
get_nTI_vectors is deprecated and physically invalid for N>2. It represents the old approach of recombining pairwise envelopes recursively – TI(TI(E1,E2), TI(E3,E4), ...) – feeding an already-modulated envelope vector back into a formula that was derived only for two carrier fields. Measured against the verified envelope on random fields, this recursive form has a signed mean error of +38.6% (range -90% to +416%) at N=4, and +103% at N=8. Calling it emits a DeprecationWarning and delegates to get_mTI_vectors – so the deprecated call still returns the correct answer, it just should not be relied on for its own (wrong) formula going forward.
Carrier Wiring (Channels)
By default, get_mTI_vectors/get_TI_avg pair fields positionally: field 0 with field 1, field 2 with field 3, and so on – each electrode pair is its own independent carrier. The channels parameter overrides this grouping explicitly:
channels = [(group_a, group_b), ...]
Each channel is a (group_a, group_b) pair of integer indices into fields. Per channel, E_a = sum(fields[i] for i in group_a) and E_b = sum(fields[i] for i in group_b) (an empty group_b sums to zeros – a non-beating carrier that contributes to P but not Q). The summed pairs from all channels are concatenated in channel order and fed to the same K-pair envelope. channels=None reproduces positional consecutive pairing byte-identically – it is not an approximation of the explicit form, it is the explicit form [([0],[1]), ([2],[3]), ...].
For a 4-pair montage, MTI_CHANNEL_ARCHITECTURES (tit/opt/config.py) exposes two named choices:
| Label | channels value |
Meaning |
|---|---|---|
| Two independent channels (default) | None |
Pairs 1&2 form one TI channel, pairs 3&4 form a second, independent TI channel |
| Four pairs, two carriers | [([0, 2], [1, 3])] |
All four pairs share two carriers – pairs 1&3 vs. 2&4 (Lee et al. 2022) |
channels=[([0, 2], [1, 3])] is algebraically exactly get_TI_vectors(E0+E2, E1+E3) – summing carriers before taking the K=1 envelope, rather than taking a 4-pair envelope over four independent carriers. The two wirings are not interchangeable. The toolbox’s regression test on random fields (tests/test_calc_mti.py) asserts that they differ by more than 5% in over half of all mesh elements; the figure recorded alongside that assertion for the montage tested is about 92% of elements, and the GUI help text notes differences of up to 6x in places.
Important: Montage.channels can only be set from a tit.sim JSON config or directly in Python (tit/sim/__main__.py:_build_channels). load_montages never reads a channels value from montage_list.json, and tit/gui/simulator_tab.py never sets Montage.channels at all – so every mTI simulation launched from the GUI runs with channels=None (positional/independent-dyad pairing). The mex-search tab’s “Carrier Wiring” combo (see GUI Walkthrough) is the one place in the GUI that does expose this choice, but only for mex-search candidates, not for simulator runs.
hf_peak/hf_sar (safety metrics, below) are unaffected by channels: they always sum over every carrier field regardless of how carriers are grouped for the envelope.
Simulator Behavior for mTI
mTISimulation (tit/sim/mTI.py) builds one SimNIBS TDCS list per pair, each driven at config.intensities[i] mA (converted to A). SimulationConfig.intensities defaults to [1.0, 1.0]; for mTI, validation requires len(config.intensities) >= montage.num_pairs – one current value per pair, not two. The GUI’s job-card current placeholder switches from 1.0,1.0 to 1.0,1.0,1.0,1.0 when the montage mode is multipolar.
Output fields. SimulationConfig.output_fields gates which volume-mesh fields are computed and written – it defaults to ["TI_max"] only. TI_avg and the safety fields (hf_peak, hf_sar) must be opted into; the four selectable names are exactly ("TI_max", "TI_avg", "hf_peak", "hf_sar") (const.SELECTABLE_OUTPUT_FIELDS). The gating skips the computation, not just the write – unrequested fields are never evaluated. In the GUI (shared by TI and mTI), output fields are checkboxes with only TI_max pre-checked; submitting with none checked is rejected with “Select at least one output field.”
On disk, the mTI mesh spells the modulation-depth field TI_Max (capital M) – the same quantity TI_max names on a 2-pair TI mesh, just a different on-disk name for the 4-pair case.
TI_normal is not computed for mTI. Standard TI derives it from SimNIBS’s 2-field TI.get_dirTI; the N-pair analogue would need the coherent K-pair envelope evaluated at a fixed direction (the surface normal) rather than maximized over direction, and while tit.calc has that primitive internally, it is only exposed as a private helper today – exposing it publicly, and updating tit/analyzer/field_selector.py (which resolves the normal mesh under TI/mesh/ unconditionally), is deferred.
fsaverage projection is skipped for mTI. SimulationConfig.map_to_fsavg defaults to True, but the projection step returns early for any montage whose simulation_mode != TI, logging “fsaverage projection: skipping %s (mTI not yet supported)”.
mTI supports an arbitrary even number of pairs, capped at 26 (A-Z pair labelling); more than 26 pairs raises ValueError. Post-processing:
- Loads and crops all N high-frequency meshes to brain tissue (
BRAIN_TISSUE_TAG_RANGES = ((1, 100), (1001, 1100))). - Computes intermediate 2-pair TI vector fields for adjacent pairs (
{montage}_TI_AB.msh,{montage}_TI_CD.msh, …) via the plain K=1get_TI_vectors– these are saved for inspection only and are not recombined into the final result (that would be the deprecated recursive path). - Computes the final envelope over all N carrier fields jointly via
get_mTI_vectors(e_fields, channels=montage.channels), written asTI_Max; optionallyTI_avg,hf_peak,hf_sarperoutput_fields. - Extracts GM/WM crops (
grey_{montage}_mTI.msh,white_{montage}_mTI.msh), generates a central cortical surface viamsh2cortex, and converts meshes to NIfTI.
Output layout, under derivatives/SimNIBS/sub-{id}/Simulations/{montage}/:
mTI/mesh/{montage}_mTI.msh # TI_Max / TI_avg / hf_peak / hf_sar (selected)
mTI/mesh/grey_{montage}_mTI.msh # GM crop
mTI/mesh/white_{montage}_mTI.msh # WM crop
mTI/mesh/surfaces/ # central cortical surface (msh2cortex)
mTI/niftis/ # mesh -> NIfTI conversion
mTI/montage_imgs/ # combined_montage_visualization.png
Per-pair high-frequency meshes are renamed TDCS_1..N -> TDCS_A..Z when moved into high_Frequency/mesh/, matching the A-Z pair labelling used everywhere else in the mTI output.
Safety Metrics
Two carrier-exposure safety metrics (tit/fields.py, Cassarà et al. 2025) are computed directly from the N per-pair carrier E-fields, independent of the modulation-depth envelope:
hf_peak (Eq. 3) is the worst-case instantaneous peak carrier field: carriers run at mutually incommensurate frequencies, so every relative phase combination occurs over time, and the true worst case is the max over sign choices, max_s |sum_i s_i * E_i| for s_i in {+1,-1}. At N=2 this is exactly max(|E1+E2|, |E1-E2|). Up to EXACT_SIGN_ENUM_MAX_FIELDS = 8 fields, this is solved by exact sign enumeration (2^(N-1) combinations – 128 at N=8). Above 8 fields the combinatorics blow up (measured ~44.6s for N=12’s 2048 combinations at 200k elements vs. ~2.0s at N=8), so a 4000-direction Fibonacci-sphere sweep picks the best-sampled support direction and evaluates the exact, realizable vector sum for the sign pattern it implies. This sweep fallback is still a lower bound on the true max over all 2^(N-1) sign combinations – since only the sampled directions’ implied patterns are tried – and is therefore slightly non-conservative.
hf_sar is sum_i |E_i|^2 in (V/m)^2 – carriers are incoherent, so their power adds rather than their amplitudes. This is a field-domain heating proxy, not calibrated SAR: the actual calibration is (sigma / 2*rho) * hf_sar, requiring per-tissue conductivity and density that the toolbox does not apply.
Both metrics always sum over every carrier field regardless of channels – carrier exposure does not depend on how pairs are grouped into TI channels for the envelope – and both are opt-in: neither is in SimulationConfig.output_fields’s default (["TI_max"]), so a run must explicitly request hf_peak/hf_sar to get them written.
Multipolar Exhaustive Search (mex-search)
tit/opt/mex/ extends ex-search to four bipolar pairs (eight electrodes), scored with the same verified get_mTI_vectors envelope used by the simulator – explicitly not a recursive envelope-of-envelopes. The public API is run_m_ex_search(config: MExConfig) -> MExResult, re-exported from tit.opt alongside run_ex_search/run_flex_search.
Configuration
MExConfig required fields (no defaults):
| Field | Type |
|---|---|
subject_id |
str |
leadfield_hdf |
str |
roi_name |
str |
electrodes |
MExConfig.BucketElectrodes or MExConfig.PoolElectrodes |
Optional fields and their exact defaults:
| Field | Default |
|---|---|
current_mA |
2.0 (mA on every pair) |
channels |
None (positional pairing; see Carrier Wiring) |
roi_radius |
3.0 mm |
roi_names |
None (single-ROI behaviour driven by roi_name; an explicit [] means no spherical centers at all, which is what a purely atlas-driven target needs) |
roi_atlas |
None |
roi_coordinate_space |
"subject" (or "mni") |
run_name |
None (defaults to a %Y%m%d_%H%M%S timestamp) |
symmetric_bucket |
False |
symmetry_eeg_csv |
None |
symmetry_pairing |
"within_pairs" (or "cross_pairs") |
current_mA <= 0 raises ValueError. symmetric_bucket=True with PoolElectrodes raises ValueError (symmetric search only supports bucket mode).
MExConfig.BucketElectrodes has exactly eight list[str] fields, in this order: e1_plus, e1_minus, e2_plus, e2_minus, e3_plus, e3_minus, e4_plus, e4_minus. MExConfig.PoolElectrodes has a single field, electrodes: list[str].
Candidate enumeration
- Bucket mode (default): the full Cartesian product of the eight buckets, keeping only tuples where all 8 electrode names are distinct (
len(set(electrodes)) == 8). - Pool mode (
PoolElectrodes,all_combinations=True):itertools.permutations(pool, 8). A pool of exactly 8 electrodes yields8! = 40,320candidates. - Symmetric bucket search (
symmetric_bucket=True, bucket mode only): restricts candidates to those whose pairs mirror left/right, using a mirror map built from an EEG-position CSV (reflecting the x-coordinate across the midline). Two pairing schemes:within_pairs(default): each pair’s own+/-electrodes must mirror each other.cross_pairs: additionally requires pair 1 <-> pair 3 and pair 2 <-> pair 4 to mirror.
This collapses the search space roughly to linear in bucket size instead of the full Cartesian product.
Scoring
For each candidate, the engine computes four leadfield fields via TI.get_field([e_a, e_b, current_mA/1000.0], leadfield, idx_lf) (mA converted to A), takes vectors = get_mTI_vectors(fields, channels=config.channels), and scores np.linalg.norm(vectors, axis=1). Per-candidate metrics (ROI-name-prefixed):
| Key | Meaning |
|---|---|
{roi}_TImax_ROI |
Max over ROI elements |
{roi}_TImean_ROI |
Volume-weighted mean over ROI elements |
{roi}_TImean_GM |
Volume-weighted mean over GM elements (SimNIBS tag 2) |
{roi}_Focality |
TImean_ROI / TImean_GM (0.0 if the GM mean is <= 0) |
{roi}_n_elements |
ROI element count |
current_ch1_mA .. current_ch4_mA |
All four set to the same current_mA |
If the ROI has zero elements, all four metrics are 0.0. ROI resolution (spherical CSV centers, NIfTI/MGZ masks, atlas label selections) is inherited wholesale from the ex-search engine.
Outputs
mex-search reuses ex-search’s output pipeline, writing exactly four files to derivatives/SimNIBS/sub-{id}/m-ex-search/{run_name}/:
run_config.json– subject_id, roi_name, roi_radius, leadfield_hdf, electrode_mode ("pool"or"bucket"), electrodes, n_combinations, run_name, current_mA.-
final_output.csv– one row per evaluated candidate, in enumeration order (not sorted). Fixed 8-column header:Montage, Current_Ch1_mA, Current_Ch2_mA, TImax_ROI, TImean_ROI, TImean_GM, Focality, Composite_Index, whereComposite_Index = TImean_ROI * Focality. Currents are formatted to 1 decimal, metrics to 4. The Montage cell strips theTI_field_/.mshwrapper from the candidate key and replaces_and_with ` <> `.Note: the engine computes
current_ch3_mA/current_ch4_mAtoo, but the CSV header has no columns for them – only Ch1/Ch2 currents are recorded (both ex-search and mex-search share this fixed-width CSV writer). montage_distributions.png– three histograms (dpi=300).intensity_vs_focality_scatter.png(dpi=300).
The candidate key/name format is TI_field_{e1a}_{e1b}_and_{e2a}_{e2b}_and_{e3a}_{e3b}_and_{e4a}_{e4b}_I-{current_mA:.1f}mA.msh – no mesh file is actually written; it is only a label. Progress is logged per candidate plus a coarse estimate every 500 candidates, since bucketed 4-pair searches can reach hundreds of thousands of combinations. SIGINT/SIGTERM sets a stop flag that breaks after the current candidate, and partial results are still written.
Running it
simnibs_python -m tit.opt.mex config.json
JSON config keys: project_dir, subject_id, leadfield_hdf, roi_name, electrodes (_type: "BucketElectrodes" or "PoolElectrodes", plus e1_plus..e4_minus or electrodes), current_mA, channels, roi_radius, roi_names, roi_atlas, roi_coordinate_space, run_name, symmetric_bucket, symmetry_eeg_csv, symmetry_pairing.
{
"project_dir": "/path/to/project",
"subject_id": "101",
"leadfield_hdf": "101_leadfield_EEG10-20_Okamoto_2004.hdf5",
"roi_name": "L-Insula.csv",
"electrodes": {
"_type": "BucketElectrodes",
"e1_plus": ["Fp1", "Fp2"],
"e1_minus": ["Pz", "Oz"],
"e2_plus": ["C3", "F3"],
"e2_minus": ["C4", "F4"],
"e3_plus": ["T7", "T8"],
"e3_minus": ["O1", "O2"],
"e4_plus": ["Fz", "Cz"],
"e4_minus": ["P3", "P4"]
},
"current_mA": 2.0,
"roi_radius": 3.0
}
GUI Walkthrough
The Ex-Search tab hosts both TI and mTI search behind a single Search Mode combo: “TI (2-pair)” (default) and “mTI (4-pair)”. Selecting mTI:
- Hides the Bucketed/All Combinations radio buttons and switches the electrode panel to eight free-text fields, E1+ .. E4- (2 columns x 4 rows). mTI is bucket-only – there is no all-combinations page, since pool permutations over eight positions are combinatorially far larger than TI’s four.
- Switches the current-configuration panel to a Pair Current (mA) spinbox (range 0.1-10.0, default 2.0, step 0.1), a Carrier Wiring combo (the two
MTI_CHANNEL_ARCHITECTURESchoices from Carrier Wiring), and a Force left/right symmetry checkbox (unchecked by default) that enables a symmetry-pairing combo – “Within each pair” / “Cross pairs (E1<->E3, E2<->E4)” – once checked. - Disables the Combine ROIs checkbox only. Both ROI types are available:
MExConfigcarriesroi_namesandroi_atlasexactly asExConfigdoes, so an mTI run can target a sphere, an atlas region, or an atlas region alone. Combining stays TI-only because the multipolar run path processes selected spheres one at a time. - Retitles the box “mTI Configuration” and relabels the run/stop buttons “Run mTI Search”/”Stop mTI Search”.
The shared ROI Radius spinbox (range 1.0-10.0 mm, default 3.0) applies to both modes.
Validation before an mTI run requires all eight bucket fields to be non-empty (“Please enter valid electrodes for all eight mTI bucket categories (E1..E4, +/-)”), plus at least one ROI selected. The confirmation dialog reports per-bucket electrode counts, pair current, carrier wiring, an upper-bound search-space size (Search Space: up to <product of the 8 bucket sizes> eight-electrode combinations, before the distinctness filter), and the ROI list.
Analyzer Behavior for mTI
The Analyzer detects an mTI simulation purely from the presence of {simulation}/mTI/mesh/ on disk – there is no separate analyzer mode to select, and the same Analyzer class handles both TI and mTI.
TI_maxandTI_Maxare treated as aliases for the same quantity: whichever spelling is requested, the analyzer resolves it to the on-disk name the detected simulation type actually wrote (TI_Maxfor mTI,TI_maxfor TI).TI_normalis unavailable for mTI: requesting it raisesFileNotFoundError(“TI_normal is only computed for standard 2-pair TI simulations”), since mTI never writes a normal-component mesh (see Simulator Behavior for mTI). Consequentlynormal_mean,normal_max, andnormal_focalityare always absent (None) from mTI analysis results.
References and Attribution
- Grossman, N. et al. (2017). Noninvasive deep brain stimulation via temporally interfering electric fields. Cell, 169(6), 1029-1041.
- Hirata, A. et al. (2024). Electric field envelope focality with linear alignment montage. Computers in Biology and Medicine, 178, 108697.
- Cassarà, A.M. et al. (2025). Recommendations for the safe application of temporal interference stimulation, Parts I-II. Bioelectromagnetics, 46(2). doi:10.1002/bem.22542.
- Botzanowski, B. et al. (2025). Focal control of non-invasive deep brain stimulation using multipolar temporal interference. Bioelectronic Medicine, 11(1), 7.
- Lee, S. et al. (2022). Multipair transcranial temporal interference stimulation for deep brain targeting. Frontiers in Neuroscience.
The K>=2 envelope, the Fibonacci-sphere direction sampling, and get_magnitude_am were ported into tit/calc.py from collaborator Larissa Albantakis’s branch alba/mTI_testing. The multipolar exhaustive search combination logic (tit/opt/mex/logic.py) and the generalized electrode-bucket loader (tit/opt/ex/buckets.py) were ported from her branch alba/ex-search-multipolar.