Repository: psychopy/psychopy

Issue:
builder: conditional start bug
In a routine, if the start of stim2 is set to be the end of stim1 (condition, value = "stim1.status == FINISHED"), then stim2 can completely fail to be shown at all (perhaps shown occasionally for 1 frame or so). Minimal builder script: one routine, two stim, with the start for the second being the end of the first, both having a 1s duration in time.

This appears to be due the routineTimer part of the while loop expression:

```
while continueRoutine and routineTimer.getTime() > 0:
```

It works properly in at least one case if I comment that out, so the loop is controlled by:

```
while continueRoutine:
```

These alternative lines are written out by experiment.py ~1204, 1206:

```
if useNonSlip:
    buff.writeIndented('while continueRoutine and routineTimer.getTime() > 0:\n')
else:
    buff.writeIndented('while continueRoutine:\n')
```

I am not well versed enough in non-slip timing to know what is going on. I have some guesses (the logic in `getMaxTime`), but not confident enough to be sure that any tweak I might try would not compromise something else.



Discussion:
Observations from Michael MacAskill:

```
I have no idea why, but for me, the problem was resolved either by putting a value in the
“Expected start (s)” field (2 in this case), OR by reducing the number of vertices from 1000
to a more reasonable number (like 90).
```

Interesting, I had not noticed that. For me the specific behavior does seem to go away with fewer vertices, in that I see something after the fixation, but I am not sure that the duration timing is right. So I think fewer vertices is not solving the problem.

Using a second textStim as the stim coming after the fixation, I only see it at all if its duration is longer than the duration of the fixation. Its as if its duration goes from the start of the trial, not from its own specified conditional duration. I suspect something is not right with the routineTimer.reset() code. No idea why the expected start should matter.


Resolving patch:
diff --git a/psychopy/app/builder/builder.py b/psychopy/app/builder/builder.py
--- a/psychopy/app/builder/builder.py
+++ b/psychopy/app/builder/builder.py
@@ -1068,7 +1068,7 @@ def drawFlowRoutine(self,dc,routine,id,pos=[0,0], draw=True):
             fontSizeDelta = (8,4,0)[self.appData['flowSize']]
             font.SetPointSize(1000/self.dpi-fontSizeDelta)
 
-        maxTime, nonSlip, onlyStaticComps = routine.getMaxTime()
+        maxTime, nonSlip = routine.getMaxTime()
         if nonSlip:
             rgbFill=nonSlipFill
             rgbEdge=nonSlipEdge
@@ -1379,9 +1379,9 @@ def redrawRoutine(self):
     def getMaxTime(self):
         """Return the max time to be drawn in the window
         """
-        maxTime, nonSlip, onlyStaticComps = self.routine.getMaxTime()
-        if onlyStaticComps:
-            maxTime= maxTime+0.5
+        maxTime, nonSlip = self.routine.getMaxTime()
+        if self.routine.hasOnlyStaticComp():
+            maxTime = int(maxTime) + 1.0
         return maxTime
     def drawTimeGrid(self, dc, yPosTop, yPosBottom, labelAbove=True):
         """Draws the grid of lines and labels the time axes
@@ -2078,8 +2078,8 @@ def _getCtrlValue(self, ctrl):
         This function checks them all and returns the value or None.
 
         .. note::
-            Don't use GetStringSelection() here to avoid that translated value 
-            is returned. Instead, use GetSelection() to get index of selection 
+            Don't use GetStringSelection() here to avoid that translated value
+            is returned. Instead, use GetSelection() to get index of selection
             and get untranslated value from _choices attribute.
         """
         if ctrl is None:
@@ -2107,7 +2107,7 @@ def _setCtrlValue(self, ctrl, newVal):
         This function checks them all and returns the value or None.
 
         .. note::
-            Don't use SetStringSelection() here to avoid using tranlated 
+            Don't use SetStringSelection() here to avoid using tranlated
             value.  Instead, get index of the value using _choices attribute
             and use SetSelection() to set the value.
         """
diff --git a/psychopy/app/builder/components/_base.py b/psychopy/app/builder/components/_base.py
--- a/psychopy/app/builder/components/_base.py
+++ b/psychopy/app/builder/components/_base.py
@@ -209,22 +209,21 @@ def getStartAndDuration(self):
             return None, None, True#this component does not have any start/stop
         startType=self.params['startType'].val
         stopType=self.params['stopType'].val
+        numericStart = canBeNumeric(self.params['startVal'].val)
+        numericStop = canBeNumeric(self.params['stopVal'].val)
         #deduce a start time (s) if possible
         #user has given a time estimate
         if canBeNumeric(self.params['startEstim'].val):
             startTime=float(self.params['startEstim'].val)
-        elif startType=='time (s)' and canBeNumeric(self.params['startVal'].val):
+        elif startType=='time (s)' and numericStart:
             startTime=float(self.params['startVal'].val)
         else: startTime=None
         #if we have an exact
-        if stopType=='time (s)' and canBeNumeric(self.params['stopVal'].val):
+        if stopType=='time (s)' and numericStop:
             duration=float(self.params['stopVal'].val)-startTime
-            nonSlipSafe=True
-        elif stopType=='duration (s)' and canBeNumeric(self.params['stopVal'].val):
+        elif stopType=='duration (s)' and numericStop:
             duration=float(self.params['stopVal'].val)
-            nonSlipSafe=True
         else:
-            nonSlipSafe=False
             #deduce duration (s) if possible. Duration used because component time icon needs width
             if canBeNumeric(self.params['durationEstim'].val):
                 duration=float(self.params['durationEstim'].val)
@@ -232,6 +231,7 @@ def getStartAndDuration(self):
                 duration=FOREVER#infinite duration
             else:
                 duration=None
+        nonSlipSafe = numericStart and numericStop
         return startTime, duration, nonSlipSafe
     def getPosInRoutine(self):
         """Find the index (position) in the parent Routine (0 for top)
diff --git a/psychopy/app/builder/experiment.py b/psychopy/app/builder/experiment.py
--- a/psychopy/app/builder/experiment.py
+++ b/psychopy/app/builder/experiment.py
@@ -1180,7 +1180,7 @@ def writeMainCode(self,buff):
         buff.writeIndented('%s.reset()  # clock \n' %(self._clockName))
         buff.writeIndented('frameN = -1\n')
         #can we use non-slip timing?
-        maxTime, useNonSlip, onlyStaticComps = self.getMaxTime()
+        maxTime, useNonSlip = self.getMaxTime()
         if useNonSlip:
             buff.writeIndented('routineTimer.add(%f)\n' %(maxTime))
 
@@ -1270,6 +1270,8 @@ def getComponentFromName(self, name):
             if comp.params['name'].val==name:
                 return comp
         return None
+    def hasOnlyStaticComp(self):
+        return all([comp.type == 'Static' for comp in self])
     def getMaxTime(self):
         """What the last (predetermined) stimulus time to be presented. If
         there are no components or they have code-based times then will default
@@ -1277,8 +1279,7 @@ def getMaxTime(self):
         """
         maxTime=0
         nonSlipSafe = True # if possible
-        onlyStaticComps = True
-        for n, component in enumerate(self):
+        for component in self:
             if 'startType' in component.params:
                 start, duration, nonSlip = component.getStartAndDuration()
                 if not nonSlip:
@@ -1292,13 +1293,10 @@ def getMaxTime(self):
                 except:
                     thisT=0
                 maxTime=max(maxTime,thisT)
-                #update onlyStaticComps if needed
-                if component.type != 'Static':
-                    onlyStaticComps = False
         if maxTime==0:#if there are no components
             maxTime=10
             nonSlipSafe=False
-        return maxTime, nonSlipSafe, onlyStaticComps
+        return maxTime, nonSlipSafe
 
 class ExpFile(list):
     """An ExpFile is similar to a Routine except that it generates its code


Test patch: