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)