Source code for psp.clobe.imac

"""The inequality matrix constructor: the ordinal pattern of a result."""

from __future__ import annotations

from collections.abc import Sequence
from functools import cache

import torch

from ._utils import Ordering, _check_thresholds, _classify, _is_weak, _tolerance

__all__ = ["imac"]


[docs] def imac( values: torch.Tensor | Sequence[float], thresholds: Sequence[float] | torch.Tensor, ordering: Ordering = "semiorder", *, inclusive: bool = False, ) -> torch.Tensor: """Inequality matrix constructor: the ordinal pattern of a result. Compares every pair of values. Above the diagonal, cell ``(i, j)`` is ``1`` if ``values[i] - values[j] > upper``, ``-1`` if it is ``< lower`` and ``0`` (approximately equal) otherwise; on and below the diagonal it is ``2``. With ``ordering="weak"`` only ties count as approximately equal, whatever the thresholds. A difference within :data:`TOLERANCE` of a threshold counts as equal to it; for float32 and lower precision, within 8 eps of the dtype. Batched over leading dimensions, so ``imac`` fits the ``discretize`` contract of :func:`psp.PSP`: ``(n, k)`` in, ``(n, k, k)`` out. Parameters ---------- values Results of shape ``(..., k)``, one value per stimulus (e.g. response probabilities). thresholds ``(upper, lower)`` with ``upper >= lower``, e.g. ``(0.1, -0.1)``. ordering ``"semiorder"`` (the default) or ``"weak"``. inclusive Count a difference equal to a threshold as larger (``>= upper``) or smaller (``<= lower``), without a tolerance, instead of approximately equal. This is the rule of clobe before 0.8.0, used by :mod:`psp.models.oscar`. Semiorders only. Returns ------- torch.Tensor Shape ``(..., k, k)``, the floating dtype of ``values`` (integers become the default dtype). A difference that is NaN stays NaN; the R package returns 0. Examples -------- >>> imac(torch.tensor([[0.4, 0.3, 0.2], [0.2, 0.2, 0.9]]), (0.1, -0.1)) tensor([[[ 2., 0., 1.], [ 2., 2., 0.], [ 2., 2., 2.]], <BLANKLINE> [[ 2., 0., -1.], [ 2., 2., -1.], [ 2., 2., 2.]]]) As the ``discretize`` of a run: >>> from functools import partial >>> discretize = partial(imac, thresholds=(0.1, -0.1)) A difference of exactly a threshold, by default and with ``inclusive``: >>> values = torch.tensor([0.5, 0.25], dtype=torch.float64) >>> imac(values, (0.25, -0.25))[0, 1].item() 0.0 >>> imac(values, (0.25, -0.25), inclusive=True)[0, 1].item() 1.0 """ upper, lower = _check_thresholds(thresholds) weak = _is_weak(ordering, inclusive) x = torch.as_tensor(values) if not x.dtype.is_floating_point: x = x.to(torch.get_default_dtype()) if x.ndim == 0: raise ValueError("'values' must have at least one dimension, one value per stimulus") difference = x.unsqueeze(-1) - x.unsqueeze(-2) # (..., k, k): values[i] - values[j] cells = _classify(difference, upper, lower, weak, _tolerance(x.dtype), inclusive) cells = torch.where(torch.isnan(difference), difference, cells) k = x.shape[-1] return torch.where(_above(k, x.device), cells, 2.0)
@cache def _above(k: int, device: torch.device) -> torch.Tensor: """The cells above the diagonal of a ``k x k`` matrix.""" return torch.ones(k, k, dtype=torch.bool, device=device).triu(diagonal=1)