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:
IntegralandDerivativeperform discrete calculus on their inputs.Smoothersmooths its inputTransitionprovides a transient (self-removing, once complete) dynamic.Oscillatorprovides 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 thatfwould otherwise have given, but whereas theTransform()method actually alters the original instancef,Applycreates a newFunctioninstance and leaves the originalfuntouched.See also:
Function.Transform
- Shady.Dynamics.CallOnce(func)
Can be used in a
Sequenceto 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
Functionwrapped 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 thetargument 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=Trueyou 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,ResetTimeBasewill not work on this object.
- Shady.Dynamics.Derivative(*pargs, **kwargs)
Like
Integral(), but configures itsFunctionto perform discrete-time differentiation instead of integration.
- Shady.Dynamics.Forever
alias of
count
- class Shady.Dynamics.Function(*pargs, **kwargs)
A
Functioninstance is a callable wrapper around another callable and/or around a constant.Functionobjects support arithmetic operators.f = Function( lambda t: t ** 2 ) + Function( lambda t: t ) g = lambda t: t ** 2 + t
fandgare both callable objects and both will return the same result when called with a numeric argument.fis 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
Functionis called.**kwargs – If supplied, any optional keyword arguments are passed through to every callable term, whenever the
Functionis called.
- Tap(initial=None)
As you transform a
Functionobject, 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
Functionbuild 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
Transformmethod 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_pargsor**kwargsare passed through to the transforming function (any_callable).The
Transform()method changes theFunctioninstance in-place, in a manner analogous to+=and*=. By contrast, binary operators*and+return a newFunctioncontaining 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
Functionbuild 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
Transformmethod 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_pargsor**kwargsare passed through to the transforming function (any_callable).The
Transform()method changes theFunctioninstance in-place, in a manner analogous to+=and*=. By contrast, binary operators*and+return a newFunctioncontaining 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,intor numericnumpyarray. The return value can be:None, in which case nothing happensa
dictd, in which case theFunctionwill raise anAbortexception (a subclass ofStopIteration) containingdas the exception’s argument. TheFunctionitself will not perform any further processing.a numeric value (or numeric array)
x, in which case theFunctionwill continue to process the numeric value but, at the very last step in the chain, it will raise aStopexception (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 withStopIterationexceptions 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
Functioninstance, which will returnmagnitudethe first time it is called (or when called again with the sametargument as its first call) and then return0.0if called with any other value oft.
- Shady.Dynamics.Integral(*pargs, **kwargs)
Returns a specially-configured
Function. Like theFunctionconstructor, the terms wrapped by this call may be numeric constants, and/or callables that take a single numeric argumentt. And like anyFunctioninstance, the instance returned byIntegralis itself a callable object that can be called witht.Unlike a vanilla
Function, however, anIntegralhas memory for values ofton 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 oftfor which the object is called.Like any
Function, it can interact with otherFunctions, with other single-argument callables, with numeric constants, and with numericnumpyobjects via the standard arithmetic operators+,-,/,*,**, and%, and may also have other functions applied to its output viaApply.Integralmay naturally be take anotherIntegraloutput 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
Functionobject with an output that oscillates sinusoidally as a function of time: the result ofApply()-ingSinusoidto 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
FunctionandStateMachineinstances. It will run through thetermsof theFunction(and recursively through all thetermsof any terms that are themselvesFunction`s) looking for dynamic objects that have memory: `StateMachine, andFunctionwrappers produced byIntegral,Derivative,TimeOut,TransitionorSmoother. In any of these cases, it erases their memory of previous calls. They (and hence theFunctionas a whole) will consider the nexttvalue they receive to be “time zero”.
- Shady.Dynamics.Sequence(container)
Returns a
Functionobject whose value is defined piecewise by the elements ofcontainer. Thecontainermay be:a
dictwhose keys are numbers: the keys are interpreted as time-points relative to the first time theFunctionis called, and they dictate the times at which theFunctionoutput should switch to the corresponding value. If any value is itself a callable object, then the overallFunctionoutput 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
Functionis called with a new value for the timetargument. If any item is itself a callable object, then the overallFunctionoutput 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 aStopIterationexception. This allows you to chainTransition()function objects together—e.g.:Sequence( [ Transition( 0, 100 ), Transition( 100, 0 ) ] )
You can also use the constant
STITCHto 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.sinifnumpyis 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. Ifnumpyis 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 aFunctionwrapper around this.
- Shady.Dynamics.Smoother(arg=None, sigma=1.0, exponent='EWA')
This function constructs a
Functioninstance 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
Functioninstance or any other callable that returns a numeric output.sigma – Interpreted as a the sigma (width) parameter of a Gaussian if
exponentis 2.0 (or of the comparable exponential-family function if it is some other positive numeric value). Ifexponentis not numeric,sigmais interpreted as the half-life of an exponential-weighted-average (EWA) smoother.exponent – If this is
Noneor the string'EWA'then theSmootheruses exponential weighted averaging, withsigmaas 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.0gets you Gaussian-weighted FIR coefficients.
- Returns:
A
Functioninstance.
- class Shady.Dynamics.StateMachine(*states)
This class encapsulates a discrete-time state machine. Instances of this class are callable, with a single argument
tfor time.You can call a
StateMachineinstance multiple times with the same value oft. Its logic will only run whentincreases.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
StateMachinedepends 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.Statesubclass.duration – A numeric constant, or
None, or a callable that returns either a numeric constant orNone. Determines the default duration of the state (Nonemeans 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 beNonebecauseNoneis a legal state for aStateMachineto be in. Or it can be left entirelyUnspecifiedin which case it means “the next state, if any, that I add to thisStateMachinewithAddState()”.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 theStateMachineis 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,offsetmay 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
stateargument 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 theAddStatemethod 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
StateMachineis called with a new timetvalue.- Parameters:
newState (str) – If omitted, change to the state dictated by the current state’s
nextattribute/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
StateMachinecall, 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 theStateMachineinstance is called with a novel timetargument.
- 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
tisNone, 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_totalsame asself.Elapsed( t=None, origin='total' )self.elapsed_currentsame asself.Elapsed( t=None, origin='current' )
- Shady.Dynamics.TimeOut(func, duration)
Returns a wrapped version of callable
functhat raises aStopexception when called with atargument larger than the very firsttargument 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 itsendvalue, aStopexception (a subclass ofStopIteration) is raised. Some frameworks (e.g.Shady.PropertyManagement) will automatically catch and deal withStopIterationexceptions.- 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
startandend, then the output range oftransformshould 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