First it would be a good idea to read the Shady documentation on Concurrency. The first two examples (under the heading "Running single-threaded") do not have the problem you're worried about, since Stimulus
instances are created before a single frame is actually rendered. Here's a simple example:
import Shady
w = Shady.World(threaded=False)
# s1 = ...
# s2 = ...
# ...
w.Run()
Beyond that point, you're right that one of Shady's inherent paradigm shifts is that you never call any flip()
equivalent yourself. And that may feel unfamiliar at first, but be assured: in the applications we've built on Shady, we do not miss the days of calling flip()
ourselves. Updates to stimulus parameters get done in callbacks, such as:
the so-called "animation callback" that you can optionally install into every World
or Stimulus
instance;
dynamic (callable) values that you can assign to any World
or Stimulus
property (these get evaluated immediately after any animation callbacks);
event callbacks that respond to key-presses, etc.
Changes that you make within the same callback are guaranteed to be rendered on the same frame—and that's the answer to how we keep tight control of timing even when not running single-threaded. It's true that if you run multi-threaded and issue one w.Stimulus()
call after another, they're not guaranteed to appear on the same frame (in fact, they are guaranteed to appear on different frames because a .Stimulus()
call in the non-drawing thread actually defers the real work of stimulus creation into the drawing thread and then waits until that has been completed before returning). The possible antidotes are (1) run single-threaded; (2) perform all w.Stimulus()
creation calls in a dedicated Prepare()
method, as in the documentation's second example; or (3) ensure the stimuli have visible=False
when created, and only make them visible later. In all of those cases, we are careful to separate creation of stimuli (which can be slow) from manipulation of their properties.
The first two callback types are most relevant to what you're describing. They are covered in Shady's documentation on "Making properties dynamic". Within the framework they provide, there are (as always in Python, and in Shady) many different ways to achieve the goal you describe. Personally, I like to use a StateMachine
instance as my animation callback. Here's how I would create a simple repetitively-presented stimulus whose onset is heralded by a sensor patch flashing for a single frame:
import random
import Shady
w = Shady.World(canvas=True, gamma=2.2)
# STIMULI
Shady.Stimulus.SetDefault(visible=False)
# let all stimuli be invisible by default when created
gabor = w.Sine(pp=0)
sensorPatch = w.Stimulus(
size = 100, # small,
color = 1, # bright,
anchor = Shady.UPPER_LEFT, # with its top-left corner stuck...
position = w.Place(Shady.UPPER_LEFT), # to the top-left corner of the screen
)
# STATE MACHINE
sm = Shady.StateMachine()
@sm.AddState
class InterTrialInterval(sm.State):
# state names should be descriptive but can be anything you want
def duration(self):
return random.uniform(1.0, 3.0)
next = 'PresentGabor'
@sm.AddState
class PresentGabor(sm.State):
def onset(self):
gabor.visible = True
sensorPatch.visible = Shady.Impulse() # a dynamic object: returns 1.0 the first time it is evaluated, then 0.0 thereafter
duration = 2.0
def offset(self):
gabor.visible = False
next = 'InterTrialInterval'
w.SetAnimationCallback( sm )
# now sm(t) will be called at every new time `t`, i.e. on every frame,
# and this will in turn call `onset()` and `offset()` whenever appropriate