Repository: psychopy/psychopy

Issue:
Coder tutorial 2 does not work as expected
The code for the second tutorial of the coder does not work as expected given that the number of trials set in the StairHandler is 1. 

See: https://github.com/psychopy/psychopy/blob/master/docs/source/coder/tutorial2.py#L28


Discussion:
The surprise is caused by the fact that multiple things control the termination of a staircase. We need to exceed both the nTrials and the number of reversals. In this example there are 7 step sizes so we'd need to get to at least 7 reversals in order to terminate.

It's a tricky one to know what to do. Personally I would usually only set up a staircase with few (minimum) reversals and 50 trials so 50 would be the genuine limit, but I know a lot of other scientists use the reversals as the limit and don't care that they have diffferent lenghs for each run.

I guess we try to make more clear in the demo why this is the case (in less text than I've just written!?) ;-)

Thanks again for pointing it out
This is actually a bug.

In `__init__()`, we do:
```python
        if nReversals is not None and len(self.stepSizes) > nReversals:
            logging.warn(
                "Increasing number of minimum required reversals to "
                "the number of step sizes (%i)." % len(self.stepSizes)
            )
            self.nReversals = len(self.stepSizes)
        else:
            self.nReversals = nReversals
```
https://github.com/psychopy/psychopy/blob/a6549c0fa3230386cecad0a2d6bb7481b1152c0f/psychopy/data.py#L2939

By default, `nReversals` is `None`. So when we execute the tutorial code, we will do
```python
        if (len(self.reversalIntensities) >= self.nReversals and
                len(self.intensities) >= self.nTrials):
            self.finished = True
```
with `nReversals=None` after adding a response.(https://github.com/psychopy/psychopy/blob/a6549c0fa3230386cecad0a2d6bb7481b1152c0f/psychopy/data.py#L3091)

And we'll assume to be "finished", in error:
```python
In[9]: print(len(self.reversalIntensities))
0
In[10]: print(self.nReversals)
None
In[11]: print(len(self.reversalIntensities) >= self.nReversals)
True
```





Resolving patch:
diff --git a/psychopy/data.py b/psychopy/data.py
--- a/psychopy/data.py
+++ b/psychopy/data.py
@@ -2936,11 +2936,12 @@ def __init__(self,
         self._variableStep = True if len(self.stepSizes) > 1 else False
         self.stepSizeCurrent = self.stepSizes[0]
 
-        if nReversals is not None and len(self.stepSizes) > nReversals:
-            logging.warn(
-                "Increasing number of minimum required reversals to "
-                "the number of step sizes (%i)." % len(self.stepSizes)
-            )
+        if nReversals is None:
+            self.nReversals = len(self.stepSizes)
+        elif len(self.stepSizes) > nReversals:
+            msg = ('Increasing number of minimum required reversals to the '
+                   'number of step sizes, (%i).' % len(self.stepSizes))
+            logging.warn(msg)
             self.nReversals = len(self.stepSizes)
         else:
             self.nReversals = nReversals


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
@@ -73,8 +73,8 @@ def checkSimulationResults(self):
             # The first trial can never be a reversal.
             assert stairs.nReversals <= len(stairs.data) - 1
 
-            assert stairs.nReversals == len(stairs.reversalPoints)
-            assert stairs.nReversals == len(stairs.reversalIntensities)
+            assert len(stairs.reversalPoints) >= stairs.nReversals
+            assert len(stairs.reversalIntensities) >= stairs.nReversals
 
         if stairs.reversalPoints:
             assert stairs.reversalIntensities
@@ -307,6 +307,22 @@ def test_StairHandlerLinearScalarStepSizeReveralsNone(self):
         self.simulate()
         self.checkSimulationResults()
 
+    def test_nReversals(self):
+        start_val = 1
+        step_sizes = range(5)
+
+        staircase = data.StairHandler(startVal=start_val, stepSizes=step_sizes,
+                                      nReversals=None)
+        assert staircase.nReversals == len(step_sizes)
+
+        staircase = data.StairHandler(startVal=start_val, stepSizes=step_sizes,
+                                      nReversals=len(step_sizes) - 1)
+        assert staircase.nReversals == len(step_sizes)
+
+        staircase = data.StairHandler(startVal=start_val, stepSizes=step_sizes,
+                                      nReversals=len(step_sizes) + 1)
+        assert staircase.nReversals == len(step_sizes) + 1
+
 
 class TestQuestHandler(_BaseTestStairHandler):
     """
@@ -330,6 +346,8 @@ def test_QuestHandler(self):
             minVal=minVal, maxVal=maxVal
         )
 
+        self.stairs.nReversals = None
+
         self.responses = makeBasicResponseCycles(
             cycles=3, nCorrect=2, nIncorrect=2, length=10
         )