Creating new PlanOut operators
This page explains the anatomy of the operator class and shows you how to create new operators for use with the PlanOut interpreter.
PlanOutOp
New PlanOut operators are created by subclassing PlanOutOp
(located in ops/base.py),
or one of its child classes (described below).
Once initialized, all operator objects have an instance variable,
self.args
that is populated with
the keyword arguments of the operator object when it is first constructed.
Developers extending PlanOutOp
implement the execute()
method, which
does computations with the self.args
input data, and returns an output
value (typically a scalar but sometimes an array).
As the PlanOut interpreter traverses serialized PlanOut code (nested dictionaries),
it checks each dictionary to see if it contains an op
key, which identifies
the dictionary is an operator. This name is the same as the
operator name to class dispatch, located in utils.ops.Operators.initFactory()
.
The initFactory()
method contains a dictionary mapping operator
names (as read by the PlanOut interpreter) to classes.
Developers also specify required and optional parameters through the
options()
method, which is used to check the correctness of PlanOut code.
The function returns a dictionary whose keys are variable names, and values
are dictionaries specifying whether the variables are required, and a description
of the variable. This data may be used to automatically construct graphical
interfaces for assembling experiments.
Let’s consider one such operator, get
, which retrieves the value of
a given given variable var
from the PlanOut execution environment:
class Get(PlanOutOp):
def options(self):
return {'var': {'required': 1, 'description': 'variable to get'}}
def execute(self, mapper):
return mapper.get(self.args['var'])
Here, we can see that the operator has one required argument: var
. Execution
occurs by implementing the execute()
method, which reads data from self.args
,
and optionally the mapper
object, and returns a value.
The mapper object contains variables from the PlanOut execution environment.
In this case, the var
argument from self.args
is used to extract a variable
from the PlanOut mapper.
SimpleOp
The execute()
method from PlanOutOp
requires that the developer manually
evaluate code input arguments as needed.
This allows developers to implement operators like ‘or’,
‘and’, and ‘cond’ (ifelse). But in many cases, we just want to use already-evaluated
arguments. This can be accomplished by sublassing SimpleOp
instead of PlanOutOp
,
and implementing the simpleExecute()
method which reads already evaluated arguments
from the dictionary, self.parameters
.
For example, array indexing is implemented as follows:
class Index(PlanOutOpSimple):
def simpleExecute(self):
return self.parameters['base'][self.parameters['index']]
Standardized operator types
Other base operator classes include those for creating
unary operators, binary operators, and “commutative” operators.
These operators subclass PlanOutOpSimple
and use
standard naming conventions.
PlanOutOpUnary
Unary ops have one required argument, ‘value’, which gets passed into the
unaryExecute()
method.
Here is how ‘Not’ (!) is implemented:
class Not(PlanOutOpUnary):
def unaryExecute(self, value):
return not value
PlanOutOpBinary
Binary operators have two required arguments, ‘left’ and ‘right’. Here is how Equals is implemented:
class Equals(PlanOutOpBinary):
def binaryExecute(self, left, right):
return left == right
PlanOutOpCommutative
Commutative operators have a single parameter called values
. Note that these
operators need not be commutative, they just take a single array as an argument.
class Min(PlanOutOpCommutative):
def commutativeExecute(self, values):
return min(values)