Shady.Dynamics Sub-module
The Dynamics module contains a number of objects that are designed to perform discretized real-time processing of arbitrary functions.
The most general-purpose object is the Function
which is
a type of callable that supports arithmetic and other tricks.
Various wrapper functions act as factories for Functions
configured with specialized behavior (most of them with
memory for previous inputs). For example:
Integral
andDerivative
perform discrete calculus on their inputs.Smoother
smooths its inputTransition
provides a transient (self-removing, once complete) dynamic.Oscillator
provides sinusoidally varying output
The other major contribution is StateMachine
a different
class that provides callable instances in which discrete-
time state-machine logic cna easily be implemented.
Note that everything exported from this module is also available in
the top-level Shady.*
namespace.
- Shady.Dynamics.Apply(any_callable, f, *additional_pargs, **kwargs)
g = Apply( some_function, f ) # `f` is a `Function` instance # and now so is `g`
is equivalent to:
g = copy.deepcopy( f ).Transform( some_function )
In both cases,
some_function()
is applied to transform the output thatf
would otherwise have given, but whereas theTransform()
method actually alters the original instancef
,Apply
creates a newFunction
instance and leaves the originalf
untouched.See also:
Function.Transform
- Shady.Dynamics.CallOnce(func)
Can be used in a
Sequence
to create a side effect. For example:def Foo(): print( 'The sequence has ended' ) f = Sequence( [ Transition( 0, 1 ), Transition( 1, 0 ), CallOnce( Foo ) ] ) import numpy for t in numpy.arange( 0, 3, 0.01): print(f(t))
- Shady.Dynamics.Clock(startNow=True, speed=1.0)
This function returns a
Function
wrapped around a simple linear callable that takes one argumentt
.- Parameters:
startNow (bool) – If this is
True
, the clock’s time zero starts the first time it is called. If it isFalse
, the clock does not subtract any “time zero”, but rather just uses thet
argument that is passed to it.speed (float) – This specifies the speed at which the clock runs—it’s just a multiplier applied to the input vaue
t
.
If
startNow=True
you can get the same effect withf = Integral( speed )
but the implementation inClock()
is simpler and hence a little more efficient. Note that, because of this simplicity,ResetTimeBase
will not work on this object.
- Shady.Dynamics.Derivative(*pargs, **kwargs)
Like
Integral()
, but configures itsFunction
to perform discrete-time differentiation instead of integration.
- Shady.Dynamics.Forever
alias of
count
- class Shady.Dynamics.Function(*pargs, **kwargs)
A
Function
instance is a callable wrapper around another callable and/or around a constant.Function
objects support arithmetic operators.f = Function( lambda t: t ** 2 ) + Function( lambda t: t ) g = lambda t: t ** 2 + t
f
andg
are both callable objects and both will return the same result when called with a numeric argument.f
is of course less efficient thang
. However, it allows functions to be defined and built up in a modular way- Parameters:
*pargs – Each of the positional arguments is considered to be a separate additive “term”. Terms (or their outputs, if callable) are added together when the
Function
is called.**kwargs – If supplied, any optional keyword arguments are passed through to every callable term, whenever the
Function
is called.
- Tap(initial=None)
As you transform a
Function
object, you develop a pipeline of successively applied operations. What if you want to examine values at an intermediate stage along that pipeline? In this case you canTap()
it. The result is a callable object that you can call to obtain the latest value.Example:
f = Function( lambda t: t ) f *= 2 f += 2 intermediate = f.Tap() f.Transform( math.sin ) for input_value in [ 0.1, 0.2, 0.3, 0.4, 0.5 ]: output_value = f( input_value ) print( ' t = %r' % input_value ) print( ' 2 * t + 2 = %r' % intermediate() ) print( 'sin(2 * t + 2) = %r' % output_value ) print( '' )
- Through(any_callable, *additional_pargs, **kwargs)
Arithmetic operations on a
Function
build up a chain of operations. For example:f = Function( lambda t: t ) f += 1 # Now f(t) will return t + 1 f *= 2 # Now f(t) will return 2 * t + 2
The
Transform
method allows arbitrary operations to be added to the chain. For example:f.Transform( math.sin ) # Now f(t) will return math.sin( 2 * t + 2 )
Any
*additional_pargs
or**kwargs
are passed through to the transforming function (any_callable
).The
Transform()
method changes theFunction
instance in-place, in a manner analogous to+=
and*=
. By contrast, binary operators*
and+
return a newFunction
containing deep copies of the original instance’s terms. As+
is to+=
, so the global functionApply()
is to theTransform()
method:f = Function( lambda t: t ) g = Apply( math.cos, f ) # creates a separate `Function` instance f.Transform( math.sin ) # transforms `f` in-place # now g(t) returns math.cos(t) and f(t) returns math.sin(t)
- Transform(any_callable, *additional_pargs, **kwargs)
Arithmetic operations on a
Function
build up a chain of operations. For example:f = Function( lambda t: t ) f += 1 # Now f(t) will return t + 1 f *= 2 # Now f(t) will return 2 * t + 2
The
Transform
method allows arbitrary operations to be added to the chain. For example:f.Transform( math.sin ) # Now f(t) will return math.sin( 2 * t + 2 )
Any
*additional_pargs
or**kwargs
are passed through to the transforming function (any_callable
).The
Transform()
method changes theFunction
instance in-place, in a manner analogous to+=
and*=
. By contrast, binary operators*
and+
return a newFunction
containing deep copies of the original instance’s terms. As+
is to+=
, so the global functionApply()
is to theTransform()
method:f = Function( lambda t: t ) g = Apply( math.cos, f ) # creates a separate `Function` instance f.Transform( math.sin ) # transforms `f` in-place # now g(t) returns math.cos(t) and f(t) returns math.sin(t)
- Watch(conditional, *additional_pargs, **kwargs)
Adds a watch condition on the output of a
Function
.- Parameters:
conditional (callable) – This should be a callable whose first input argument is a
float
,int
or numericnumpy
array. The return value can be:None
, in which case nothing happensa
dict
d
, in which case theFunction
will raise anAbort
exception (a subclass ofStopIteration
) containingd
as the exception’s argument. TheFunction
itself will not perform any further processing.a numeric value (or numeric array)
x
, in which case theFunction
will continue to process the numeric value but, at the very last step in the chain, it will raise aStop
exception (a subclass ofStopIteration
) containing the final processed value.
*additional_pargs – If additional positional arguments are supplied, they are simply passed through to
conditional
.**kwargs – If additional keyword arguments are supplied, they are simply passed through to
conditional
.
Some frameworks (e.g.
Shady.PropertyManagement
) will automatically catch and deal withStopIteration
exceptions appropriately, but if you need to do so manually, you can do so as follows:try: y = f( t ) except StopIteration as exc: info = exc.args[ 0 ] if not isinstance( info, dict ): terminal_value_of_y = info
- Shady.Dynamics.Impulse(magnitude=1.0, autostop=True)
This function constructs a very simple specially-configured
Function
instance, which will returnmagnitude
the first time it is called (or when called again with the samet
argument as its first call) and then return0.0
if called with any other value oft
.
- Shady.Dynamics.Integral(*pargs, **kwargs)
Returns a specially-configured
Function
. Like theFunction
constructor, the terms wrapped by this call may be numeric constants, and/or callables that take a single numeric argumentt
. And like anyFunction
instance, the instance returned byIntegral
is itself a callable object that can be called witht
.Unlike a vanilla
Function
, however, anIntegral
has memory for values oft
on which it has previously been called, and returns the cumulative area under the sum of its wrapped terms, estimated discretely via the trapezium rule at the distinct values oft
for which the object is called.Like any
Function
, it can interact with otherFunctions
, with other single-argument callables, with numeric constants, and with numericnumpy
objects via the standard arithmetic operators+
,-
,/
,*
,**
, and%
, and may also have other functions applied to its output viaApply
.Integral
may naturally be take anotherIntegral
output as its input, or indeed any other type ofFunction
.Example - prints samples from the quadratic \(\frac{1}{2}t^2 + 100\):
g = Integral( lambda t: t ) + 100.0 print( g(0) ) print( g(0.5) ) print( g(1.0) ) print( g(1.5) ) print( g(2.0) )
- Shady.Dynamics.Oscillator(freq, phase_deg=0.0)
Returns a
Function
object with an output that oscillates sinusoidally as a function of time: the result ofApply()
-ingSinusoid
to anIntegral
.
- Shady.Dynamics.RaisedCosine(x)
Maps a linear ramp from 0 to 1 onto a raised-cosine rise from 0 to 1. Half a Hann window.
- Shady.Dynamics.ResetTimeBase(x)
This method can be applied to
Function
andStateMachine
instances. It will run through theterms
of theFunction
(and recursively through all theterms
of any terms that are themselvesFunction`s) looking for dynamic objects that have memory: `StateMachine
, andFunction
wrappers produced byIntegral
,Derivative
,TimeOut
,Transition
orSmoother
. In any of these cases, it erases their memory of previous calls. They (and hence theFunction
as a whole) will consider the nextt
value they receive to be “time zero”.
- Shady.Dynamics.Sequence(container)
Returns a
Function
object whose value is defined piecewise by the elements ofcontainer
. Thecontainer
may be:a
dict
whose keys are numbers: the keys are interpreted as time-points relative to the first time theFunction
is called, and they dictate the times at which theFunction
output should switch to the corresponding value. If any value is itself a callable object, then the overallFunction
output is computed by calling it, with the relative time-since-first call as its single argument.any other iterable: the items are then simply handled in turn, each time the
Function
is called with a new value for the timet
argument. If any item is itself a callable object, then the overallFunction
output is computed by calling it, with the relative time-since-first call as its single argument—also, we do not advance to the next item until that item has raised aStopIteration
exception. This allows you to chainTransition()
function objects together—e.g.:Sequence( [ Transition( 0, 100 ), Transition( 100, 0 ) ] )
You can also use the constant
STITCH
to ensure that the terminal value from the preceding callable is ignored—so in the following example, the value 100 would not be repeated:Sequence( [ Transition( 0, 100 ), STITCH, Transition( 100, 0 ) ] )
- Shady.Dynamics.Sinusoid(cycles, phase_deg=0)
Who enjoys typing
2.0 * numpy.pi *
over and over again? This is a wrapper aroundnumpy.sin
(ormath.sin
ifnumpy
is not installed) which returns a sine function of an argument expressed in cycles (0 to 1 around the circle). Heterogeneously, but hopefully intuitively, the optional phase-offset argument is expressed in degrees. Ifnumpy
is installed, either argument may be non-scalar (phase_deg=[90,0]
is useful for converting an angle into 2-D Cartesian coordinates).This is a function, but not a
Function
. You may be interested inOscillator
, which returns aFunction
wrapper around this.
- Shady.Dynamics.Smoother(arg=None, sigma=1.0, exponent='EWA')
This function constructs a
Function
instance that smooths, with respect to time, the numeric output of whatever callable object it wraps. You could test this by wrapping the output ofImpulse()
with it.- Parameters:
arg – A
Function
instance or any other callable that returns a numeric output.sigma – Interpreted as a the sigma (width) parameter of a Gaussian if
exponent
is 2.0 (or of the comparable exponential-family function if it is some other positive numeric value). Ifexponent
is not numeric,sigma
is interpreted as the half-life of an exponential-weighted-average (EWA) smoother.exponent – If this is
None
or the string'EWA'
then theSmoother
uses exponential weighted averaging, withsigma
as its half-life. Alternatively, if this is a positive floating- point value, it is treated as the exponent of an exponential-family function for generating finite-impulse- response weights, with sigma as the time-scale parameter.exponent=2.0
gets you Gaussian-weighted FIR coefficients.
- Returns:
A
Function
instance.
- class Shady.Dynamics.StateMachine(*states)
This class encapsulates a discrete-time state machine. Instances of this class are callable, with a single argument
t
for time.You can call a
StateMachine
instance multiple times with the same value oft
. Its logic will only run whent
increases.Example:
sm = StateMachine() def PrintName( state ): print( state.name ) sm.AddState( 'First', next='Second', duration=1.0, onset=PrintName ) sm.AddState( 'Second', next='First', duration=2.0, onset=PrintName ) import time while True: time.sleep( 0.1 ) sm( time.time() )
The magic of a
StateMachine
depends on how the states are defined. This can be done in a number of ways, the most powerful of which is to define each state as a subclass ofStateMachine.State
.See the
AddState()
method for more details.- AddState(state, duration=None, next=Unspecified, onset=None, ongoing=None, offset=None)
Add a new state definition to a
StateMachine
.- Parameters:
state – This can be a string, defining the name of a new state. Or it can be a class that inherits from
StateMachine.State
. Or it can be an actual instance of such aStateMachine.State
subclass.duration – A numeric constant, or
None
, or a callable that returns either a numeric constant orNone
. Determines the default duration of the state (None
means indefinite).next – A string, or a callable that returns a string, specifying the state to change to when the duration elapses, or when
ChangeState()
is called without specifying a destination. May also beNone
becauseNone
is a legal state for aStateMachine
to be in. Or it can be left entirelyUnspecified
in which case it means “the next state, if any, that I add to thisStateMachine
withAddState()
”.onset – Either
None
, or a callable routine that will get called whenever we enter this state.ongoing – Either
None
, or a callable routine that will get called whenever theStateMachine
is called and we are currently in the state. If the callableongoing()
returns a string, the state machine will immediately attempt to change to the state named by that string.offset – Either
None
, or a callable routine that will get called whenever we leave this state.
duration
,next
,onset
,ongoing
,offset
may be constants, callables that take no arguments, or callables that take one argument. If they accept an argument, that argument will be an instance ofStateMachine.State
. This means they are effectively methods of yourState
, and indeed can be defined that way if you prefer.Since it is legal for the
state
argument to be a class definition, and for all the other arguments to be defined as attributes or methods of that class (instead of as arguments to this method), one valid way to use theAddState
method is as a class decorator:import random sm = StateMachine() @sm.AddState class First( StateMachine.State ): next = 'Second' duration = 1.0 def onset( self ): print( self.name ) @sm.AddState class Second( StateMachine.State ): next = 'First' def onset( self ): print( self.name ) def duration( self ): return random.uniform( 1, 5 ) def ongoing( self ): if self.elapsed > 3.0: print( 'we apologize for the delay...' )
Equivalently, you also can do:
class First( StateMachine.State ): ... class Second( StateMachine.State ): ... sm = StateMachine( First, Second )
Equivalent state-machines can be defined without using the object-oriented class-definition approach: the syntax is simpler for very simple cases, but quickly explodes in complexity for more sophisticated machines. Here is an example, simplified relative to the above:
sm = StateMachine() def PrintName( state ): print( state.name ) sm.AddState( 'First', next='Second', duration=1.0, onset=PrintName ) sm.AddState( 'Second', next='First', duration=2.0, onset=PrintName )
- ChangeState(newState=StateMachine.NEXT, timeOfChange=StateMachine.PENDING)
This method manually requests a change of state, the next time the
StateMachine
is called with a new timet
value.- Parameters:
newState (str) – If omitted, change to the state dictated by the current state’s
next
attribute/method. Otherwise, attempt to change to the state named by this argument.timeOfChange (float) – This is used internally. To ensure accuracy, you should not specify this yourself. When called from outside the stack of an ongoing
StateMachine
call, this method actually only requests a state change, and the change itself will happen on, and be timed according to, the next occasion on which the theStateMachine
instance is called with a novel timet
argument.
- Elapsed(t=None, origin='total')
Return the amount of time elapsed at time
t
, as measured either from the very first call to theStateMachine
(origin='total'
) or from the most recent state change (origin='current'
).If
t
isNone
, then the method returns the time elapsed at the most recent call to theStateMachine
, a result that can also be obtained from two special properties:self.elapsed_total
same asself.Elapsed( t=None, origin='total' )
self.elapsed_current
same asself.Elapsed( t=None, origin='current' )
- Shady.Dynamics.TimeOut(func, duration)
Returns a wrapped version of callable
func
that raises aStop
exception when called with at
argument larger than the very firstt
argument it receives plusduration
.
- Shady.Dynamics.Transition(start=0.0, end=1.0, duration=1.0, delay=0.0, transform=None, finish=None)
This is a self-stopping dynamic. It uses a
Function.Watch()
call to ensure that, when the dynamic reaches itsend
value, aStop
exception (a subclass ofStopIteration
) is raised. Some frameworks (e.g.Shady.PropertyManagement
) will automatically catch and deal withStopIteration
exceptions.- Parameters:
start (float, int or numeric numpy.array) – initial value
end (float, int or numeric numpy.array) – terminal value
duration (float or int) – duration of the transition, in seconds
delay (float or int) – delay before the start of the transition, in seconds
transform (callable) – an optional single-argument function that takes in numeric values in the domain [0, 1] inclusive, and outputs numeric values. If you want the final output to scale correctly between
start
andend
, then the output range oftransform
should also be [0, 1].finish (callable) – an optional zero-argument function that is called when the transition terminates
Example:
from Shady import World, Transition, RaisedCosine, Hann w = World( canvas=True, gamma=-1 ) gabor = w.Sine( pp=0, atmosphere=w ) @w.EventHandler( slot=-1 ) def ControlContrast( self, event ): if event.type == 'key_release' and event.key == 'r': gabor.contrast = Transition( transform=RaisedCosine ) if event.type == 'key_release' and event.key == 'f': gabor.contrast = Transition( 1, 0, transform=RaisedCosine ) if event.type == 'key_release' and event.key == 'h': gabor.contrast = Transition( duration=2, transform=Hann ) # press 'f' to see contrast fall from 1 to 0 using a raised-cosine # press 'r' to see contrast rise from 0 to 1 using a raised-cosine # press 'h' to see contrast rise and fall using a Hann window in time