Skip to content

cutoffs

tit.plotting.nilearn.cutoffs

Turn a :class:~tit.plotting.nilearn.config.NilearnConfig's cutoffs into V/m.

Its own module rather than a helper inside __main__: the rule here is arithmetic over one array and has to be testable without importing the runner (which pulls in nibabel, nilearn and :mod:tit.stats), and the same rule is what a caller would want before submitting a job.

The failure this exists to prevent: min_cutoff used to default to a fixed 0.3 V/m, which is above every real TI field -- ernie's MNI-space L_Insula TI_max peaks at 0.1385 V/m -- so the default run resolved an inverted display range and died three frames deep inside matplotlib with minvalue must be less than or equal to maxvalue (job 287e3e0c2eb94134), a message that named neither the cutoff nor the data.

See Also

tit.plotting.nilearn.main : Resolves the cutoffs here, before any figure is drawn.

field_summary

field_summary(values: ndarray) -> str

One human sentence describing the field the cutoffs are being judged against.

Source code in tit/plotting/nilearn/cutoffs.py
def field_summary(values: np.ndarray) -> str:
    """One human sentence describing the field the cutoffs are being judged against."""
    if values.size == 0:
        return "the averaged field has no non-zero voxels"
    return (
        f"the averaged field spans {float(values.min()):.4f}-{float(values.max()):.4f} V/m "
        f"over {values.size} non-zero voxels "
        f"(median {float(np.percentile(values, 50.0)):.4f}, "
        f"p{DEFAULT_MIN_PERCENTILE:g} {float(np.percentile(values, DEFAULT_MIN_PERCENTILE)):.4f}, "
        f"p{DEFAULT_MAX_PERCENTILE:g} {float(np.percentile(values, DEFAULT_MAX_PERCENTILE)):.4f})"
    )

blank_figure_warning

blank_figure_warning(values: ndarray, min_cutoff: float) -> str | None

One warning line when min_cutoff is at or above every voxel, else None.

:func:resolve_cutoffs only refuses an empty range (min >= max). A caller can still ask for a floor that is above the data while keeping a ceiling above that -- which is exactly what the desktop panel's former default does (min_cutoff 0.3 V/m with max_cutoff 5, measured in tests/smoke/payloads/nilearn.json): the job then succeeds and writes PDFs with nothing on them. That is the same failure as the crash, only silent, so the runner says so in the job log rather than leaving the reader to wonder why the brain is empty.

Source code in tit/plotting/nilearn/cutoffs.py
def blank_figure_warning(values: np.ndarray, min_cutoff: float) -> str | None:
    """One warning line when *min_cutoff* is at or above every voxel, else ``None``.

    :func:`resolve_cutoffs` only refuses an *empty* range (``min >= max``). A caller can
    still ask for a floor that is above the data while keeping a ceiling above that --
    which is exactly what the desktop panel's former default does (``min_cutoff`` 0.3 V/m
    with ``max_cutoff`` 5, measured in `tests/smoke/payloads/nilearn.json`): the job then
    succeeds and writes PDFs with nothing on them. That is the same failure as the crash,
    only silent, so the runner says so in the job log rather than leaving the reader to
    wonder why the brain is empty.
    """
    if values.size == 0:
        return None
    peak = float(values.max())
    if min_cutoff < peak:
        return None
    return (
        f"min_cutoff {min_cutoff:.4f} V/m is at or above the field's peak {peak:.4f} V/m, "
        f"so the figures will be blank: {field_summary(values)}. "
        f"Leave min_cutoff unset for the data-driven default "
        f"(p{DEFAULT_MIN_PERCENTILE:g} = "
        f"{float(np.percentile(values, DEFAULT_MIN_PERCENTILE)):.4f} V/m)."
    )

resolve_cutoffs

resolve_cutoffs(values: ndarray, min_cutoff: float | None, max_cutoff: float | None, *, use_percentiles: bool = False) -> tuple[float, float]

The display range in V/m, as two concrete numbers.

Parameters

values : numpy.ndarray The averaged field's non-zero voxels (any shape; only its values matter). min_cutoff, max_cutoff : float or None The config's cutoffs. None means "take it from the data" (:data:DEFAULT_MIN_PERCENTILE / :data:DEFAULT_MAX_PERCENTILE). use_percentiles : bool, optional Read both given cutoffs as percentiles (0-100) of values instead of V/m.

Returns

tuple of float (min_cutoff, max_cutoff) in V/m, with the minimum strictly below the maximum.

Raises

ValueError If values is empty, or the resolved minimum is not below the resolved maximum. The message names the field's own range and the way out -- see the module docstring for the traceback it replaces.

Source code in tit/plotting/nilearn/cutoffs.py
def resolve_cutoffs(
    values: np.ndarray,
    min_cutoff: float | None,
    max_cutoff: float | None,
    *,
    use_percentiles: bool = False,
) -> tuple[float, float]:
    """The display range in V/m, as two concrete numbers.

    Parameters
    ----------
    values : numpy.ndarray
        The averaged field's non-zero voxels (any shape; only its values matter).
    min_cutoff, max_cutoff : float or None
        The config's cutoffs. ``None`` means "take it from the data"
        (:data:`DEFAULT_MIN_PERCENTILE` / :data:`DEFAULT_MAX_PERCENTILE`).
    use_percentiles : bool, optional
        Read both given cutoffs as percentiles (0-100) of *values* instead of V/m.

    Returns
    -------
    tuple of float
        ``(min_cutoff, max_cutoff)`` in V/m, with the minimum strictly below the maximum.

    Raises
    ------
    ValueError
        If *values* is empty, or the resolved minimum is not below the resolved maximum.
        The message names the field's own range and the way out -- see the module
        docstring for the traceback it replaces.
    """
    if values.size == 0:
        raise ValueError(
            "no non-zero field values in the averaged data: nothing to threshold or plot"
        )

    if use_percentiles:
        lo_pct = DEFAULT_MIN_PERCENTILE if min_cutoff is None else float(min_cutoff)
        hi_pct = DEFAULT_MAX_PERCENTILE if max_cutoff is None else float(max_cutoff)
        lo = float(np.percentile(values, lo_pct))
        hi = float(np.percentile(values, hi_pct))
        given = (
            f"min_cutoff {lo_pct:g}% -> {lo:.4f} V/m, "
            f"max_cutoff {hi_pct:g}% -> {hi:.4f} V/m"
        )
    else:
        lo = (
            float(np.percentile(values, DEFAULT_MIN_PERCENTILE))
            if min_cutoff is None
            else float(min_cutoff)
        )
        hi = (
            float(np.percentile(values, DEFAULT_MAX_PERCENTILE))
            if max_cutoff is None
            else float(max_cutoff)
        )
        given = f"min_cutoff {lo:.4f} V/m, max_cutoff {hi:.4f} V/m"

    if lo >= hi:
        raise ValueError(
            f"display range is empty ({given}): {field_summary(values)}. "
            f"Lower min_cutoff below {hi:.4f} V/m, set use_percentiles=true to give it "
            f"as a percentile, or leave min_cutoff unset for the data-driven default "
            f"(p{DEFAULT_MIN_PERCENTILE:g} = "
            f"{float(np.percentile(values, DEFAULT_MIN_PERCENTILE)):.4f} V/m)."
        )
    return lo, hi