Source code for quantum_mind.families.perception.models

r"""Bistable perception: how an ambiguous stimulus flips between two interpretations.

A Necker cube, a rotating silhouette or an ambiguous object seen by a robot's camera is perceived as
one of two interpretations, which alternate. The time spent in one interpretation (the *dwell time*)
is the standard measurement.

**Models.**

* :class:`QuantumZenoBistableModel` (Atmanspacher, Filk and Römer, Biological Cybernetics 90, 33-40,
  2004): the percept is a qubit whose two basis states are the two interpretations. Between two
  "observations" (internal sampling of the percept, every :math:`\Delta t`) the state rotates freely,
  :math:`U = e^{-i g \Delta t\, \sigma_x}`; each observation collapses it. The probability of a switch at
  one observation is :math:`q = \sin^2(g\,\Delta t)`, so dwell times are geometric with mean
  :math:`\Delta t / \sin^2(g \Delta t) \approx 1 / (g^2 \Delta t)`: *more frequent observation slows the
  switching* (the quantum Zeno effect).
* :class:`MarkovSwitchingModel`: classical baseline; switches occur at a constant rate :math:`r`, so dwell
  times are exponential and do not depend on the observation interval.
* :class:`GammaRenewalModel`: the standard empirical description of dwell times (Levelt), a gamma
  distribution with shape :math:`k` and scale :math:`\theta`.

**What distinguishes them.** With one observation interval the Zeno and Markov models predict the same
(geometric or exponential) shape; the gamma model fits the typical unimodal histograms better. The
distinctive prediction of the Zeno model is the dependence on :math:`\Delta t`: halving the observation
interval should double the mean dwell time. Design conditions with different intervals (for example
different presentation or attention-sampling rates) to test it.

**Design.** ``{condition: (dt, bin_edges)}`` with the observation interval and the histogram bins (in
seconds); built with :func:`dwell_time_design`. Outcomes: the probability of a dwell time falling in each
bin (multinomial, with a final bin for longer dwell times). Data: dwell-time counts per bin, built from
raw dwell times with :func:`dwell_counts`.
"""
from __future__ import annotations

import numpy as np
from scipy.stats import gamma as gamma_dist
from ...core import Model, Param

__all__ = ['QuantumZenoBistableModel', 'MarkovSwitchingModel', 'GammaRenewalModel', 'dwell_time_design',
           'dwell_counts', 'mean_dwell_time']


[docs] def dwell_time_design(dts, max_time=20.0, n_bins=20): """Design with equal-width dwell-time bins for several observation intervals. Parameters ---------- dts : dict or sequence ``{condition: dt}`` or a sequence of intervals (conditions named ``'dt=<value>'``). max_time : float, optional Upper edge of the last finite bin, in seconds. n_bins : int, optional Number of finite bins (one more bin collects longer dwell times). Returns ------- dict ``{condition: (dt, bin_edges)}``. """ if not isinstance(dts, dict): dts = {'dt=%g' % d: d for d in dts} edges = np.linspace(0, max_time, n_bins + 1) return {c: (float(d), edges) for c, d in dts.items()}
[docs] def dwell_counts(dwell_times, edges): """Histogram of dwell times, with a final bin for times beyond the last edge. Parameters ---------- dwell_times : array_like Observed dwell times in seconds. edges : array_like Bin edges. Returns ------- numpy.ndarray Counts, length ``len(edges)``. """ t = np.asarray(dwell_times, float) c, _ = np.histogram(np.clip(t, edges[0], None), bins=edges) return np.r_[c, np.sum(t >= edges[-1])]
def _cdf_bins(cdf, edges): """Bin probabilities from a CDF, with the tail probability as the last entry.""" F = np.asarray([cdf(e) for e in edges], float) return np.clip(np.r_[np.diff(F), 1 - F[-1]], 0, None)
[docs] def mean_dwell_time(model, dt): """Mean dwell time predicted by a bistable-perception model. Parameters ---------- model : QuantumZenoBistableModel, MarkovSwitchingModel or GammaRenewalModel dt : float Observation interval (ignored by the Markov and gamma models). Returns ------- float Seconds. """ return model.mean_dwell(dt)
[docs] class QuantumZenoBistableModel(Model): """Quantum Zeno model of bistable perception. Parameters ---------- g : float Free switching frequency (rad/s) of the unobserved percept. """ PARAMS = [Param('g', 'positive', 1.0)] name = 'Quantum Zeno'
[docs] def switch_probability(self, dt): """Probability of a switch at one observation. Parameters ---------- dt : float Observation interval. Returns ------- float :math:`\\sin^2(g\\,\\Delta t)`. """ return float(np.clip(np.sin(self.g * dt) ** 2, 1e-12, 1.0))
[docs] def mean_dwell(self, dt): """Mean dwell time :math:`\\Delta t / \\sin^2(g \\Delta t)`. Parameters ---------- dt : float Returns ------- float """ return dt / self.switch_probability(dt)
[docs] def predict(self, design=None): """Dwell-time bin probabilities for each condition (geometric in units of ``dt``).""" design = design or dwell_time_design([0.07]) out = {} for c, (dt, edges) in design.items(): q = self.switch_probability(dt) # P(dwell <= t) = 1 - (1 - q)^floor(t / dt) out[c] = _cdf_bins(lambda t: 1 - (1 - q) ** np.floor(t / dt + 1e-9), edges) return out
[docs] class MarkovSwitchingModel(Model): """Classical baseline: switches at a constant rate (exponential dwell times, no Zeno effect). Parameters ---------- rate : float Switching rate (1/s); the mean dwell time is ``1 / rate``. """ PARAMS = [Param('rate', 'positive', 0.3)] name = 'Markov switching'
[docs] def mean_dwell(self, dt=None): """Mean dwell time ``1 / rate`` (independent of the observation interval). Parameters ---------- dt : float, optional Ignored. Returns ------- float """ return 1 / self.rate
[docs] def predict(self, design=None): """Dwell-time bin probabilities (exponential distribution) for each condition.""" design = design or dwell_time_design([0.07]) return {c: _cdf_bins(lambda t: 1 - np.exp(-self.rate * t), edges) for c, (dt, edges) in design.items()}
[docs] class GammaRenewalModel(Model): """Classical baseline: gamma-distributed dwell times (the usual empirical description). Parameters ---------- shape : float Gamma shape :math:`k` (k = 1 is the exponential). scale : float Gamma scale :math:`\\theta` (seconds); the mean is :math:`k\\theta`. """ PARAMS = [Param('shape', 'positive', 3.0), Param('scale', 'positive', 1.0)] name = 'Gamma renewal'
[docs] def mean_dwell(self, dt=None): """Mean dwell time :math:`k\\theta` (independent of the observation interval). Parameters ---------- dt : float, optional Ignored. Returns ------- float """ return self.shape * self.scale
[docs] def predict(self, design=None): """Dwell-time bin probabilities (gamma distribution) for each condition.""" design = design or dwell_time_design([0.07]) dist = gamma_dist(self.shape, scale=self.scale) return {c: _cdf_bins(dist.cdf, edges) for c, (dt, edges) in design.items()}