Repository: psychopy/psychopy

Issue:
QUEST psychometric function
It seems #2716 should be reverted and the documentation improved to clarify things. x-ref
https://github.com/Psychtoolbox-3/Psychtoolbox-3/issues/515


Discussion:
Agreed, I'm comfortable with whatever you decide. I didn't write any part of that code. I probably won't update the docs online now until the 3.3 release

Resolving patch:
diff --git a/psychopy/contrib/quest.py b/psychopy/contrib/quest.py
--- a/psychopy/contrib/quest.py
+++ b/psychopy/contrib/quest.py
@@ -61,10 +61,16 @@ class QuestObject(object):
 
     The Weibull psychometric function:
 
-    p2=delta*gamma+(1-delta)*(1-(1-gamma)*exp(-10**(beta*(x-xThreshold))))
-
-    where x represents log10 contrast relative to threshold. The
-    Weibull function itself appears only in recompute(), which uses
+    p2=delta*gamma+(1-delta)*(1-(1-gamma)*exp(-10**(beta*(x2+xThreshold))))
+
+    where x2 represents log10 intensity relative to threshold
+    (i.e., x2 = x - T, where x is intensity, and T is threshold intensity).
+    xThreshold shifts the psychometric function along the intensity axis
+    such that threshold performance (specified as pThreshold below) will
+    occur at intensity x = T, i.e., x2 = x - T = 0. In the 
+    Watson & Pelli (1983) paper, xThreshold is denoted as epsilon and used
+    to perform testing at the "ideal sweat factor".
+    The Weibull function itself appears only in recompute(), which uses
     the specified parameter values in self to compute a psychometric
     function and store it in self. All the other methods simply use
     the psychometric function stored as instance
diff --git a/psychopy/data/staircase.py b/psychopy/data/staircase.py
--- a/psychopy/data/staircase.py
+++ b/psychopy/data/staircase.py
@@ -692,12 +692,18 @@ class QuestHandler(StairHandler):
     Measures threshold using a Weibull psychometric function. Currently, it is
     not possible to use a different psychometric function.
 
-    Threshold 't' is measured on an abstract 'intensity' scale, which
-    usually corresponds to log10 contrast.
-
-    The Weibull psychometric function:
-
-    Psi(x) = delta * gamma + (1-delta) * (1 - (1 - gamma) * exp(-10 ** (beta * (x - xThreshold))))
+    The Weibull psychometric function is given by the formula
+    
+    :math:`\Psi(x) = \delta \gamma + (1 - \delta) [1 - (1 - \gamma)\, \exp(-10^{\beta (x - T + \epsilon)})]`
+
+    Here, :math:`x` is an intensity or a contrast (in log10 units), and :math:`T` is estimated threshold.
+    
+    Quest internally shifts the psychometric function such that intensity at the user-specified
+    threshold performance level ``pThreshold`` (e.g., 50% in a yes-no or 75% in a 2-AFC task) is euqal to 0.
+    The parameter :math:`\epsilon` is responsible for this shift, and is determined automatically based on the
+    specified ``pThreshold`` value. It is the parameter Watson & Pelli (1983) introduced to perform measurements
+    at the "optimal sweat factor". Assuming your ``QuestHandler`` instance is called ``q``, you can retrieve this
+    value via ``q.epsilon``.
 
     **Example**::
 
@@ -883,6 +889,9 @@ def gamma(self):
     @property
     def delta(self):
         return self._quest.delta
+    @property
+    def epsilon(self):
+        return self._quest.xThreshold
 
     @property
     def grain(self):


Test patch:
diff --git a/psychopy/tests/test_data/test_StairHandlers.py b/psychopy/tests/test_data/test_StairHandlers.py
--- a/psychopy/tests/test_data/test_StairHandlers.py
+++ b/psychopy/tests/test_data/test_StairHandlers.py
@@ -595,6 +595,33 @@ def test_json_dump_and_reopen_file(self):
         q_loaded = fromFile(path)
         assert q == q_loaded
 
+    def test_epsilon(self):
+        # Values used by Harvey (1986), Table 3.
+        beta = 3.5
+        gamma = 0
+        delta = 0
+        epsilon = 0.0571
+        
+        # QuestHandler needs this, but it doesn't influence our
+        # test. Values chosen arbitrarily.
+        startVal = 0.5
+        startValSd = 1
+        
+        # Estimate the target proportion correct based on epsilon.
+        # (The values provided by Harvey (1986) are rounded to two
+        #  decimal places and, therefore, too imprecise. So we
+        #  have to we calculate it again.)
+        def weibull(x, beta, gamma, delta):
+            p = delta*gamma + (1-delta) * (1 - (1-gamma) * np.exp(-10 ** (beta*x)))
+            return p
+
+        p = weibull(x=epsilon, beta=beta, gamma=gamma, delta=delta)
+        
+        q = data.QuestHandler(startVal=startVal, startValSd=startValSd,
+                              pThreshold=p, beta=beta, gamma=gamma, delta=delta)
+        
+        assert np.isclose(q.epsilon, epsilon, atol=1e-4)
+
 
 class TestPsiHandler(_BaseTestStairHandler):
     def test_comparison_equals(self):