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:

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 that f would otherwise have given, but whereas the Transform() method actually alters the original instance f, Apply creates a new Function instance and leaves the original f 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))

See also: Sequence, WaitUntil, TimeOut, STITCH

Shady.Dynamics.Clock(startNow=True, speed=1.0)

This function returns a Function wrapped around a simple linear callable that takes one argument t.

Parameters:
  • startNow (bool) – If this is True, the clock’s time zero starts the first time it is called. If it is False, the clock does not subtract any “time zero”, but rather just uses the t 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 with f = Integral( speed ) but the implementation in Clock() 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 its Function 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 and g are both callable objects and both will return the same result when called with a numeric argument. f is of course less efficient than g. 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 can Tap() 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 the Function instance in-place, in a manner analogous to += and *=. By contrast, binary operators * and + return a new Function containing deep copies of the original instance’s terms. As + is to +=, so the global function Apply() is to the Transform() 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 the Function instance in-place, in a manner analogous to += and *=. By contrast, binary operators * and + return a new Function containing deep copies of the original instance’s terms. As + is to +=, so the global function Apply() is to the Transform() 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 numeric numpy array. The return value can be:

    • None, in which case nothing happens

    • a dict d, in which case the Function will raise an Abort exception (a subclass of StopIteration) containing d as the exception’s argument. The Function itself will not perform any further processing.

    • a numeric value (or numeric array) x, in which case the Function will continue to process the numeric value but, at the very last step in the chain, it will raise a Stop exception (a subclass of StopIteration) 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 with StopIteration 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 return magnitude the first time it is called (or when called again with the same t argument as its first call) and then return 0.0 if called with any other value of t.

Shady.Dynamics.Integral(*pargs, **kwargs)

Returns a specially-configured Function. Like the Function constructor, the terms wrapped by this call may be numeric constants, and/or callables that take a single numeric argument t. And like any Function instance, the instance returned by Integral is itself a callable object that can be called with t.

Unlike a vanilla Function, however, an Integral has memory for values of t 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 of t for which the object is called.

Like any Function, it can interact with other Functions, with other single-argument callables, with numeric constants, and with numeric numpy objects via the standard arithmetic operators +, -, /, *, **, and %, and may also have other functions applied to its output via Apply.

Integral may naturally be take another Integral output as its input, or indeed any other type of Function.

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 of Apply()-ing Sinusoid to an Integral.

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 and StateMachine instances. It will run through the terms of the Function (and recursively through all the terms of any terms that are themselves Function`s) looking for dynamic objects that have memory: `StateMachine, and Function wrappers produced by Integral, Derivative, TimeOut, Transition or Smoother. In any of these cases, it erases their memory of previous calls. They (and hence the Function as a whole) will consider the next t value they receive to be “time zero”.

Shady.Dynamics.Sequence(container)

Returns a Function object whose value is defined piecewise by the elements of container. The container may be:

  • a dict whose keys are numbers: the keys are interpreted as time-points relative to the first time the Function is called, and they dictate the times at which the Function output should switch to the corresponding value. If any value is itself a callable object, then the overall Function 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 time t argument. If any item is itself a callable object, then the overall Function 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 a StopIteration exception. This allows you to chain Transition() 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 ) ] )
    

See also: CallOnce, WaitUntil, TimeOut, STITCH

Shady.Dynamics.Sinusoid(cycles, phase_deg=0)

Who enjoys typing 2.0 * numpy.pi * over and over again? This is a wrapper around numpy.sin (or math.sin if numpy 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. If numpy 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 in Oscillator, which returns a Function 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 of Impulse() 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). If exponent 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 the Smoother uses exponential weighted averaging, with sigma 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 of t. Its logic will only run when t 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 of StateMachine.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 a StateMachine.State subclass.

  • duration – A numeric constant, or None, or a callable that returns either a numeric constant or None. 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 be None because None is a legal state for a StateMachine to be in. Or it can be left entirely Unspecified in which case it means “the next state, if any, that I add to this StateMachine with AddState()”.

  • 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 the StateMachine is called and we are currently in the state. If the callable ongoing() 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 of StateMachine.State. This means they are effectively methods of your State, 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 the AddState 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 time t 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 the StateMachine instance is called with a novel time t argument.

Elapsed(t=None, origin='total')

Return the amount of time elapsed at time t, as measured either from the very first call to the StateMachine (origin='total') or from the most recent state change (origin='current').

If t is None, then the method returns the time elapsed at the most recent call to the StateMachine, a result that can also be obtained from two special properties:

  • self.elapsed_total same as self.Elapsed( t=None, origin='total' )

  • self.elapsed_current same as self.Elapsed( t=None, origin='current' )

Shady.Dynamics.TimeOut(func, duration)

Returns a wrapped version of callable func that raises a Stop exception when called with a t argument larger than the very first t argument it receives plus duration.

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 its end value, a Stop exception (a subclass of StopIteration) is raised. Some frameworks (e.g. Shady.PropertyManagement) will automatically catch and deal with StopIteration 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 and end, then the output range of transform 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
Shady.Dynamics.WaitUntil(func, ongoingValue=None, finalValue=None)

Can be used in a Sequence to hold until a certain condition is fulfilled (signalled by func() returning a truthy value).

See also: Sequence, CallOnce, TimeOut, STITCH