examples/sharing.py
This is one of the example scripts included
with Shady. These scripts can be run conventionally like any
normal Python script, or you can choose to run them as
interactive tutorials, for example with python -m Shady demo sharing
#!/usr/bin/env python
# $BEGIN_SHADY_LICENSE$
#
# This file is part of the Shady project, a Python framework for
# real-time manipulation of psychophysical stimuli for vision science.
#
# Copyright (c) 2017-2022 Jeremy Hill, Scott Mooney
#
# Shady is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see http://www.gnu.org/licenses/ .
#
# $END_SHADY_LICENSE$
#: Property-sharing is a powerful tool for managing stimulus dynamics
"""
This demo shows how individual properties can be shared between different
Shady stimulus objects.
Often, you will want certain groups of `Shady.Stimulus` instances to
share one or more properties. The advantages of this are twofold.
Firstly, you save time, as you only need to adjust the properties of
one linked stimulus in order to affect all the stimuli linked to it.
Secondly, you save memory, as any shared properties among linked
stimuli will use the exact same value (or array of values).
Once shared, properties can be "un-shared" at any time and can have
their values adjusted individually.
"""#.
if __name__ == '__main__':
"""
Let's start by creating a World, configured according to
whatever command-line arguments you supplied. We'll use the
canvas by default, with a gamma of 2.2, so we test out some
of the useful sharing techniques later on.
"""#:
import Shady
cmdline = Shady.WorldConstructorCommandLine( canvas=True )
cmdline.Help().Finalize()
world = Shady.World( gamma=2.2, **cmdline.opts )
"""
Let's create some rectangles:
"""#:
a = world.Stimulus( size=[400,200], color=[1.0, 0.0, 0.0], bgalpha=0, x=-300, rotation=45 )
b = world.Stimulus( size=[200,100], color=[0.0, 0.0, 1.0], bgalpha=0, x=+300 )
c = world.Stimulus( size=[100, 50], color=[0.0, 0.7, 0.0], bgalpha=0, y=+300 )
d = world.Stimulus( size=[100, 50], color=[0.7, 0.7, 0.0], bgalpha=0, y=-300 )
"""
Let's say `b` wants to be at the same angle as `a`. Well, on
one level that's trivial:
"""#:
b.rotation = a.rotation
"""
But when `a` moves on, `b` is left behind:
"""#:
a.rotation += 30
"""
Let's use a slightly different syntax:
"""#:
b.rotation = a # what??
"""
That's right, it looked like we were trying to assign a
Stimulus *instance* to a (usually numeric) property of
another Stimulus---doesn't make much sense, right?
But notice how the rectangles are aligned again? In fact,
that was a syntactic shortcut for property sharing.
The `.rotation` properties of `a` and `b` now share the
same memory location. A change to `a` will affect `b`:
"""#:
a.rotation = 0 # affects BOTH `a` and `b`
"""
...and (because they share the same memory) vice versa:
"""#:
b.rotation = 90 # ALSO affects both
"""
The power of this idea becomes clear when you want
properties to be dynamic. Let's attach a dynamic to the
`.rotation` of `a`:
"""#:
a.rotation = lambda t: t * 45
"""
Now, a single piece of code is running on every frame,
and dumping its result into the memory location of
`a.rotation`. But since the memory location is now
shared, that single piece of code can now simultaneously
affect two stimuli (and could affect any number of
stimuli) with no extra overhead cost.
"""#:
"""
How do we make `b` independent again? Using the same
shorthand, we can tell `b` to "be itself" rather than
trying to be somebody else:
"""#:
b.rotation = b
# The instantaneous value of `b.rotation` does not
# change, but it becomes unlinked from `a.rotation`.
# And since `a`, not `b`, is the stimulus that has
# the dynamic `lambda` function running on it, `b`
# stops rotating.
"""
The full verbose form of these operations is:
"""#:
b.LinkPropertiesWithMaster( a, 'rotation' ) # same as b.rotation = a
# now they're spinning together again
"""
and...
"""#:
b.MakePropertiesIndependent( 'rotation' ) # same as b.rotation = b
# now they're independent again
"""
Note the plural "Properties" in those method names: one of the
advantages of this more-verbose syntax is that you can link or
unlink *multiple* properties in the same call. Optionally, you
can also *set* their values in the same breath, using keyword
arguments:
"""#:
b.LinkPropertiesWithMaster( a, 'color', 'envelopeSize', plateauProportion=0 )
# establish a link between `a` and `b` on all three properties:
# .color, .envelopeSize and .plateauProportion
# At the same time, set plateauProportion=0 which will then
# affect both stimuli.
"""
The same goes for declaring independence:
"""#:
b.MakePropertiesIndependent( 'plateauProportion', 'envelopeSize', color=[0,0,1] )
# uncouple .plateauProportion and .envelopeSize but don't change their values;
# uncouple .color and immediately change it back to blue
"""
There's a third method, `.ShareProperties()` that works from the
perspective of the "master" Stimulus. The main advantage to that
is that you can propagate the sharing relationship to multiple
*stimuli* in one call...
"""#:
a.ShareProperties( [ b, c, d ], 'rotation' )
"""
...as well as multiple properties. And again, it allows you the
option of setting one or more properties explicitly at the same
time, using keyword arguments:
"""#:
a.ShareProperties( b, c, d, 'envelopeSize', plateauProportion=1 )
"""
Note that only some attributes - specifically, fully-fledged
`ManagedProperty` attributes, can be shared. The `.x` attribute
alone, for example, cannot be shared, because it is a
`ManagedShortcut` to only a single element of the
`.envelopeTranslation` array, and you are limited to being able
to share property arrays entirely or not at all. Try it yourself:
I'm not going to do it, because that would crash the script,
but you can try either or both of the following by typing them at
the prompt::
c.x = a # wrong
a.ShareProperties( c, 'x' ) # equivalently wrong
"""#:
"""
Also, you cannot share *unmanaged* properties like `.frame`
or `.page` or `.text`. Again, feel free to try it::
c.frame = a # wrong
a.ShareProperties( c, 'frame' ) # equivalently wrong
"""#:
"""
To link a shortcut or unmanaged property, the best you
can do is create a dynamic:
"""#:
c.x = lambda t: a.x
# ... but of course that comes at an extra computational
# cost, on each frame (small, in this case, but in complex
# stimulus arrangements such costs can quickly add up)
"""
Anyway, now the two stimuli will automatically move together
in just the x dimension:
"""#:
a.xy = Shady.Transition(
duration = 3,
transform = lambda p: Shady.Sinusoid( 3.75 * p ) * 150 - 150,
)
"""
There is, however, one 'virtual' managed property that
exists principally to facilitate sharing. It is a shorthand
for a bundle of managed properties that affect linearization
and dynamic range enhancement.
Such things matter in visual psychophysics, so let's
illustrate with a psychophysics-y stimulus:
"""#:
gabor = world.Stimulus(
signalFunction = Shady.SIGFUNC.SinewaveSignal,
signalAmplitude = 0.5,
plateauProportion = 0.0,
)
# or, you know, sigfunc=1, siga=0.5, pp=0
# (depends where you like your code to reside on
# the concise <-> readable spectrum)
"""
It's immediately, visibly obvious that we have failed to
match the stimulus `.gamma` to that of the surrounding
`World` and its canvas. .gamma is one of the bundle of
properties we're talking about, which we collectively
call the `.atmosphere`:
"""#:
gabor.atmosphere = world
# or equivalently: gabor.LinkAtmosphereWithMaster( world )
"""
Now the whole set of properties is matched, and will
track, the `World`. This becomes obvious if we take leave
of our senses and start changing them in real time:
"""#:
world.ditheringDenominator = 3
world.gamma = Shady.Oscillator( 1.0 ) * 0.5 + 2.2
"""
Seriously though, stop that:
"""#:
world.Set( ditheringDenominator=world.dacMax, gamma=2.2 )
"""
Side note: in fact, the `World` itself has no direct
use for these properties: their visible effects are
actually mediated by a `Stimulus` called "canvas". But
the `World` has them, as placeholders. When a canvas
is created (either by using the `canvas=True` constructor
argument when creating the `World()`, or by a later
explicit call to `world.MakeCanvas()`) these properties
get shared between the `World` and its canvas, using
exactly the kind of property-sharing mechanism we're
learning about today. So then any change to these
properties of the `World` will affect the canvas, and
vice versa.
You can learn more about the canvas and the "atmosphere"
properties by looking at the `PreciseControlOfLuminance`
topic documentation:
"""#:
help( Shady.Documentation.PreciseControlOfLuminance )
# press Q to exit the help viewer
"""
The concept of a "master" is a fairly weak one, since any
change to the property values is symmetric between master
and followers. Being the "master" means two things:
1. when the sharing link is first made, the master's
current property values are preserved and the followers'
values are overwritten (unless the value is overridden
explicitly by a keyword argument in the method call,
as we saw above).
"""#:
"""
2. depending on the rendering back-end you're using,
it may be meaningless for the "master" to declare
independence. But it is always meaningful for
the followers to declare independence.
(This is a difference between the `ShaDyLib` binary
accelerator and the pure-Python `PyEngine`. We may
try to iron this out in future, one way or the other,
but it's a low priority.) Let's see what happens
there:
"""#:
a.MakePropertiesIndependent( 'rotation' )
# If you're using the accelerator (ShaDyLib) for rendering,
# nothing will happen - all colored blobs will keep rotating.
# But if you have disabled the accelerator (for example, by
# starting this script with --backend=pyglet --acceleration=False)
# then `a` will break away and keep spinning while the others
# will stop.
"""
Let's ensure things are back the way they were:
"""#:
a.ShareProperties( b, c, d, 'rotation' ) # all spinning again, if they weren't before
"""
Another unrelated potential gotcha is that you need
to remember how dynamic properties work: they install
a small subroutine that is associated with the rendering
of a *particular* Stimulus on each frame, and which dumps
its results into the memory space of that Stimulus.
This computation survives turning invisible:
"""#:
a.visible = 0
"""
...but not if that Stimulus leaves the stage entirely:
"""#:
a.Leave() # all the others stop, because the dynamic
# was specific to the `a` stimulus
"""
They're still linked:
"""#:
b.rotation += 90 # all change
"""
...in both directions:
"""#:
a.rotation += 90 # all change again
"""
...but a dynamic...
"""#:
a.rotation = lambda t: t * 45
"""
...only runs when `a` is on-stage:
"""#:
a.Enter( visible=1 )
"""
Good luck sharing properties!
"""#>
Shady.AutoFinish( world )