Operator Overview¶
In Lazyflow computations are encapsulated by so called operators, the inputs and results of a computation are provided through named slots. A computation that works on two input arrays and provides one result array could be represented like this:
from lazyflow.graph import Operator, InputSlot, OutputSlot
from lazyflow.stype import ArrayLike
class SumOperator(Operator):
inputA = InputSlot(stype=ArrayLike) # define an inputslot
inputB = InputSlot(stype=ArrayLike) # define an inputslot
output = OutputSlot(stype=ArrayLike) # define an outputslot
The above operator justs specifies its inputs and outputs, the actual definition of the computation is still missing. When another operator or the user requests the result of a computation from the operator, its execute method is called. The methods receives as arguments the outputs slot that was queried and the requested region of interest:
class SumOperator(Operator):
inputA = InputSlot(stype=ArrayLike)
inputB = InputSlot(stype=ArrayLike)
output = OutputSlot(stype=ArrayLike)
def execute(self, slot, subindex, roi, result):
# the following two lines query the inputs of the
# operator for the specififed region of interest
a = self.inputA.get(roi).wait()
b = self.inputB.get(roi).wait()
# the result of the computation is written into the
# pre-allocated result array
result[...] = a+b
Connecting operators and providing input¶
To chain multiple calculations the input and output slots of operators can be connected:
op1 = SumOperator()
op2 = SumOperator()
op2.inputA.connect(op1.output)
The input of an operator can either be the output of another operator, or the input can be specified directly via the setValue method of an input slot:
op1.inputA.setValue(numpy.zeros((10,20)))
op1.inputB.setValue(numpy.ones((10,20)))
Performing calculations¶
The result of a computation from an operator can be requested from the output slot by calling one of the following methods:
__getitem__(slicing)
: the usual [] array access operator is also provided and supports normal python slicing syntax (no strides!):
request1 = op1.output[:]
request2 = op1.output[0:10,0:20]
__call__( start, stop )
: the call method of the outputslot expects two keyword arguments, namely the start and the stop of the region of interest window of a multidimensional numpy array:
# request result via the __call__ method:
request2 = op1.output(start = (0,0), stop = (10,20))
get(roi)
: the get method of an outputslot requires as argument an existing roi object (as in the “execute” method of the example operator):
# request result via the get method and an existing roi object
request3 = op1.output.get(some_roi_object)
It should be noted that a query to an outputslot does not return the final calculation result. Instead a handle for the running calculation is returned, a so called Request object.
Request objects¶
All queries to output slots return Request objects. These requests are processed in parallel by a set of worker threads.
request1 = op1.output[:]
request2 = op2.output[:]
request3 = op3.output[:]
request4 = op4.output[:]
These request objects provide several methods to obtain the final result of the computation or to get a notification of a finished computation.
Synchronous waiting for a calculation
request = op1.output[:] result = request.wait()
after the wait method returns, the result objects contains the actual array that was requested.
Asynchronous notification of finished calculations
request = op1.output[:] def callback(request): result = request.wait() # request.wait() will return immediately # and just provide the result # do something useful with the result.. # register the callback function # it is called once the calculation is finished # or immediately if the calculation is already done. request.notify(callback)
Specification of destination result area. Sometimes it is useful to tell an operator where to put the results of its computation, when handling large numpy arrays this may save copying the array around in memory.
# create a request request = op1.output[:] a = numpy.ndarray(op1.output.meta.shape, dtype = op1.output.meta.dtype) # specify a destination array for the request result = request.writeInto(a) # when the request.wait() method returns, a will # hold the result of the calculation request.wait()
Note
writeInto()
can also be combined withnotify()
instead ofwait()
When writing operators the execute method obtains its input for the calculation from the input slots in the same manner.
Meta data¶
The input and output slots of operators have associated meta data which is held in a .meta dictionary.
The content of the dictionary depends on the operator, since the operator is responsible to provide meaningful meta information on its output slots.
Examples of often available meta information are the shape, dtype and axistags in the case of ndarray slots.
op1.output.meta.shape # the shape of the result array
op1.output.meta.dtype # the dtype of the result array
op1.output.meta.axistags # the axistags of the result array
# for more information on axistags, consult the vigra manual
When writing an operator the programmer must implement the setupOutputs method of the Operator. This method is called once all neccessary inputs for the operator have been connected (or have been provided directly via setValue).
A simple example for the SumOperator is given below:
class SumOperator(Operator):
inputA = InputSlot(stype=ArrayLike)
inputB = InputSlot(stype=ArrayLike)
output = OutputSlot(stype=ArrayLike)
def setupOutputs(self):
# query the shape of the operator inputs
# by reading the input slots meta dictionary
shapeA = self.inputA.meta.shape
shapeB = self.inputB.meta.shape
# check that the inputs are compatible
assert shapeA == shapeB
# setup the meta dictionary of the output slot
self.output.meta.shape = shapeA
# setup the dtype of the output slot
self.output.meta.dtype = self.inputA.meta.dtype
def execute(self, slot, subindex, roi, result):
pass
Propagating changes in the inputs¶
lazyflow operators should propagate changes in its inputs to their outputs. Since the exact mapping from inputs to outputs depends on the computation the operator implements, only the operator knows how the state of its outputs changes when an inputslot is modified.
To support the efficient propagation of information about changes operators should implement the propagateDirty method. This method is called from the outside whenever one of the inputs (or only part of an input) of an operator is changed.
Depending on the calculation which the operator computes the programmer should implement the correct mapping from changes in the inputs to changes in the outputs - which is fairly easy for the simple sum operator:
class SumOperator(Operator):
inputA = InputSlot(stype=ArrayLike)
inputB = InputSlot(stype=ArrayLike)
output = OutputSlot(stype=ArrayLike)
def propagateDirty(self, slot, subindex, roi):
# the method receives as argument the slot
# which was changed, and the region of interest (roi)
# that was changed in the slot
# in this case the mapping of the dirty
# region is fairly simple, it corresponds exactly
# to the region of interest that was changed in
# one of the input slots
self.output.setDirty(roi)
def setupOutputs(self):
pass
def execute(self, slot, subindex, roi, result):
pass
Wrapup: Writing an Operator¶
To implement a lazyflow operator one should:
- create a subclass of the Operator base class
- define the InputSlots and OutputSlots of the computation
- implement the setupOutputs methods to set up the meta information of the output slots depending on the meta information which is available on the input slots.
- implement the execute method, that is called when an outputslot is queried for results.
- implement the propagateDirty method, which is called when a region of interest of an input slot is changed.
class SumOperator(Operator):
inputA = InputSlot(stype=ArrayLike)
inputB = InputSlot(stype=ArrayLike)
output = OutputSlot(stype=ArrayLike)
def setupOutputs(self):
pass
def execute(self, slot, subindex, roi, result):
pass
def propagateDirty(self, slot, subindex, roi):
pass