Skip to content

fields

tit.fields

Carrier-derived high-frequency field metrics — single source of truth.

Safety metrics computed from per-pair carrier E-field vector arrays (shape (..., 3) each), following Cassarà et al. 2025, Recommendations for the Safe Application of Temporal Interference Stimulation in the Human Brain Part I (Bioelectromagnetics 46(2), e22542) and Part II (46(1), e22536).

The governing rule is Part II, p. 8: "In the presence of multiple currents (e.g., TIS channels), coherent field superposition was used for identical frequencies, and incoherent superposition (i.e., SAR addition) was used when the frequencies differed." Fields at one frequency are summed as vectors first; only then do distinct carriers combine — in power for time-averaged quantities, and by worst-case relative phase for the peak.

Which fields share a frequency is fixed by the montage, and since v2.5.0 the toolbox has exactly one wiring: positional. electrode_pairs are taken two at a time, each pair driven at its own carrier frequency, so one field is one carrier and the coherent sum within a frequency is the identity. (The optional Lee-2022 style channels grouping, where several pairs shared one carrier, was removed on main — see 7a5ee2dd, d4706e5a, b19a1c26. If it ever returns, the coherent pre-sum goes here.) These functions therefore take the carrier fields positionally and sum them incoherently.

hf_peak(*fields) — peak carrier field. Distinct carriers run at mutually incommensurate frequencies, so every relative phase combination occurs over time; the worst-case instantaneous magnitude is the max over sign choices, max_s |sum_c s_c * E_c| over carriers c. At two carriers this is exactly max(|E1+E2|, |E1-E2|) (Cassarà Part I, Eq. 3, p. 11: the worst case is "in-phase, spatially aligned fields").

hf_sar(*fields) — heating driver, proportional to SAR. Distinct carriers are incoherent, so power adds rather than amplitude: sum_c |E_c|^2 (Cassarà Part I, p. 11: "the SAR distributions from the two channels, rather than the E-fields themselves, must be summed"; Part II, p. 16: total power deposition "is equal to the summed combination from all channels (incoherent field superposition)").

Both are distinct from the stimulation-relevant modulation envelope (TI_max / TI_normal), computed in :mod:tit.calc.

See Also

tit.sim.TI : Writes hf_peak / hf_sar as volume fields on the TI mesh. tit.sim.mTI : Writes hf_peak / hf_sar as volume fields on the mTI mesh. tit.source.fsaverage : Projects hf_peak / hf_sar onto fsaverage.

hf_peak_is_exact

hf_peak_is_exact(n_fields: int) -> bool

Is :func:hf_peak exact for this many carriers, or a lower bound?

True up to EXACT_SIGN_ENUM_MAX_FIELDS carriers, where every one of the 2**(C-1) sign patterns is enumerated. Above that the direction sweep tries only the sign patterns implied by sampled directions, so the result is a lower bound on the true worst-case peak and is therefore slightly non-conservative as a safety metric. Callers that record or display hf_peak should carry this flag alongside the value.

With the positional wiring one field is one carrier, so n_fields is the carrier count.

Source code in tit/fields.py
def hf_peak_is_exact(n_fields: int) -> bool:
    """Is :func:`hf_peak` exact for this many carriers, or a lower bound?

    ``True`` up to `EXACT_SIGN_ENUM_MAX_FIELDS` carriers, where every one of
    the ``2**(C-1)`` sign patterns is enumerated.  Above that the direction
    sweep tries only the sign patterns implied by sampled directions, so the
    result is a **lower bound** on the true worst-case peak and is therefore
    slightly non-conservative as a safety metric.  Callers that record or
    display ``hf_peak`` should carry this flag alongside the value.

    With the positional wiring one field is one carrier, so *n_fields* is the
    carrier count.
    """
    return int(n_fields) <= EXACT_SIGN_ENUM_MAX_FIELDS

hf_peak

hf_peak(*fields) -> ndarray

Peak carrier field: max over sign choices of the carrier vector sum.

Each field is one carrier (positional wiring). Carriers combine at their worst-case relative phase, which for incommensurate frequencies is realised over time: max_s |sum_c s_c E_c|. At two carriers this is Cassarà et al. 2025 Part I, Eq. 3 (p. 11), max(|E1+E2|, |E1-E2|).

Exact sign enumeration (2**(C-1) combinations over C carriers) is used up to EXACT_SIGN_ENUM_MAX_FIELDS. Above that, a Fibonacci-sphere direction sweep picks the best-sampled direction and evaluates the exact, realizable vector sum for the sign pattern it implies -- tighter than a raw support-function value, but still a lower bound (hence slightly non-conservative) since only sampled directions' sign patterns are tried. Query :func:hf_peak_is_exact for which path a montage takes.

Parameters

*fields : array-like, shape (..., 3) Two or more carrier E-field vectors (one per electrode pair), all the same shape.

Returns

numpy.ndarray, shape (...,) The worst-case peak carrier field magnitude.

Source code in tit/fields.py
def hf_peak(*fields) -> np.ndarray:
    """Peak carrier field: max over sign choices of the carrier vector sum.

    Each field is one carrier (positional wiring).  Carriers combine at their
    worst-case relative phase, which for incommensurate frequencies is
    realised over time:
    ``max_s |sum_c s_c E_c|``.  At two carriers this is Cassarà et al. 2025
    Part I, Eq. 3 (p. 11), ``max(|E1+E2|, |E1-E2|)``.

    Exact sign enumeration (``2**(C-1)`` combinations over ``C`` carriers) is
    used up to `EXACT_SIGN_ENUM_MAX_FIELDS`. Above that, a Fibonacci-sphere
    direction sweep picks the best-sampled direction and evaluates the exact,
    realizable vector sum for the sign pattern it implies -- tighter than a
    raw support-function value, but still a lower bound (hence slightly
    non-conservative) since only sampled directions' sign patterns are tried.
    Query :func:`hf_peak_is_exact` for which path a montage takes.

    Parameters
    ----------
    *fields : array-like, shape ``(..., 3)``
        Two or more carrier E-field vectors (one per electrode pair), all
        the same shape.

    Returns
    -------
    numpy.ndarray, shape ``(...,)``
        The worst-case peak carrier field magnitude.
    """
    stack, shape = _stack_fields(fields)
    n = stack.shape[0]
    if n <= EXACT_SIGN_ENUM_MAX_FIELDS:
        flat = _hf_peak_exact(stack)
    else:
        flat = _hf_peak_sweep(stack)
    return flat.reshape(shape[:-1])

hf_sar

hf_sar(*fields) -> ndarray

Incoherent carrier heating driver, proportional to SAR: sum_c |E_c|^2.

Each field is one carrier (positional wiring), and the carriers sit at different, incommensurate frequencies, so their SAR/power adds rather than their amplitudes — Cassarà et al. 2025 Part II, p. 8: "coherent field superposition was used for identical frequencies, and incoherent superposition (i.e., SAR addition) was used when the frequencies differed."

This is a field-domain proxy in (V/m)^2, not calibrated SAR. With E_c the sinusoidal amplitude (peak, as the paper's thresholds are stated), the time-averaged SAR of Part II Eq. 1 is (sigma / 2 rho) * hf_sar — the 1/2 being the sinusoid's time-average, applied once here and nowhere else in the toolbox. Equivalently the RMS-squared carrier field is hf_sar / 2.

Parameters

*fields : array-like, shape (..., 3) Two or more carrier E-field vectors (one per electrode pair), all the same shape.

Returns

numpy.ndarray, shape (...,) sum_c |E_c|^2 in (V/m)^2 — proportional to tissue heating.

Source code in tit/fields.py
def hf_sar(*fields) -> np.ndarray:
    """Incoherent carrier heating driver, proportional to SAR: ``sum_c |E_c|^2``.

    Each field is one carrier (positional wiring), and the carriers sit at
    different, incommensurate frequencies, so their SAR/power adds rather
    than their amplitudes — Cassarà et al. 2025 Part II, p. 8: *"coherent
    field superposition was used for identical frequencies, and incoherent
    superposition (i.e., SAR addition) was used when the frequencies
    differed."*

    This is a field-domain proxy in ``(V/m)^2``, **not** calibrated SAR.
    With ``E_c`` the sinusoidal *amplitude* (peak, as the paper's thresholds
    are stated), the time-averaged SAR of Part II Eq. 1 is
    ``(sigma / 2 rho) * hf_sar`` — the ``1/2`` being the sinusoid's
    time-average, applied once here and nowhere else in the toolbox.
    Equivalently the RMS-squared carrier field is ``hf_sar / 2``.

    Parameters
    ----------
    *fields : array-like, shape ``(..., 3)``
        Two or more carrier E-field vectors (one per electrode pair), all
        the same shape.

    Returns
    -------
    numpy.ndarray, shape ``(...,)``
        ``sum_c |E_c|^2`` in ``(V/m)^2`` — proportional to tissue heating.
    """
    stack, shape = _stack_fields(fields)
    flat = np.sum(np.linalg.norm(stack, axis=-1) ** 2, axis=0)
    return flat.reshape(shape[:-1])