What to consider when writing an Operator in Python

Even though SNAP is based on the Java programming language it supports also the development of operator plugins written in Python. This is made possible by the snappy module. It enables Python developers to write operators for SNAP. In the background, snappy uses the library jpy which builds a bridge between Java and Python.
The intention of this page is to point out where writing an operator in Python is different from writing an operator in Java and what you need to consider. In general, the concepts presented in How to create a new operatorHow to integrate an operator, and Operator Implementation Guidelines also apply here. This page will focus on pointing out where they are different. A working example of a Python operator plugin can be found in the snap-examples repository on GitHub. The RUT project provides an example of a Python-based Operator in operational use. As you will use the Java API of SNAP Engine via snappy, you should familiarize yourself with this API, or at least know where and how to look things up. 

Table of Contents

Prerequisites 

In order to write a Python plugin for SNAP, you need the following tools:

In order to make the development easier, we recommend PyCharm as your IDE. Also, before starting to implement an operator you should make sure that you have followed the procedure described in Configure Python to use the SNAP-Python (snappy) interface.

Project Structure

The recommended basic project structure differs slightly from the structure of a Java project. It looks as shown in the following code block.

Project Structure
my-module/
    src/main/
        python/my_python_op.py
        python/my_python_op-info.xml
        nbm/manifest.mf
        resources/META-INF/services/org.esa.snap.python.gpf.PyOperatorSpi
        resources/layer.xml
pom.xml

The main differences are that you use (1) a Python file for your operator (obviously), (2) a dedicated .xml-file to set the operator metadata, and (3) a Python-specific OperatorSpi-file to register the Operator in SNAP. These files will be explained below in detail. In the pom.xml you only need to make the usual changes which are explained at How to integrate an operator. Special adaptations are not necessary for a python operator.

Operator Implementation

An Operator implemented in Python needs to have three methods. The initialize() - method , one of the three methods to compute ( doExecute(), computeTile() or computeTileStack() ) and the dispose() method. Please have a look at the following skeleton of an operator.

Operator skeleton
class MyOp:

    def __init__(self):
        pass
 
    def initialize(self, context):
        pass
 
    # You need to either implement this, the computeTile method or the computeTileStack method
    def doExecute(self, pm):
        pass

    # You need to either implement this, the doExecute method or the computeTileStack method 
    def computeTile(self, context, band, tile):
        pass
 
    # You either implement this, the doExecute method or the computeTile method
    def computeTileStack(self, context, target_tiles, target_rectangle):
        pass
 
    def dispose(self, context):
        pass

The __init__() can be dropped if you find it is not necessary. The meaning and use of the other methods is discussed in How to create a new operator and Operator Implementation Guidelines.

The context object you see as a parameter to all of the methods gives you to some general useful methods.

MethodDescription
context.getSourceTile(rasterDataNode, rectangle)Retrieves the source data from the specified raster data node and the specified rectangle. (see API Doc)
context.getSourceTile(rasterDataNode, rectangle, broderExtender)Retrieves the source data from the specified raster data node and the specified rectangle. If the rectangle exceeds the bounds of the source the borderExtender defines how to handle this case. (see API Doc)
context.getParameter('parameterName')Retrieves the value of the named parameter. (see API Doc)
context.getSourceProduct()Get the source product which shall be processed. (see API Doc)
context.getSourceProduct('sourceProductName')Get the named source product which shall be processed. (see API Doc)

By extending the rectangle used with one of the getSourceTile() methods you can get data which is outside of the target region which is currently processed.

Operator Metadata

While for Java Operators the Metadata is set through Annotations, in Python an XML file serves this purpose. This file must start with the same name as the module in which the operator is implemented and must end with -info.xml. The file is used to define processing parameters, the source product(s) and some more metadata about the operator like an operator alias name, description, version etc.

info.xml
<operator>
    <name>org.me.MyPyOp</name>
    <alias>my_py_op</alias>
    <operatorClass>org.esa.snap.python.gpf.PyOperator</operatorClass>
    <version>1.0</version>
    <authors>ACME Guys</authors>
    <copyright>(C) 2016 ACME</copyright>
    <description>
        A short description of the operator
    </description>
    <namedSourceProducts>
        <sourceProduct>
            <name>source</name>
        </sourceProduct>
    </namedSourceProducts>
    <parameters>
        <parameter>
            <name>factor</name>
            <description>Short description of the parameter</description>
            <dataType>double</dataType>
            <defaultValue>1.0</defaultValue>
        </parameter>
    </parameters>
</operator>

A full example for such an XML file can be found in the example repository. In the following, the meaning and the usage of the tags are described.

XML TagDescription
<name>A unique identifier within SNAP. The identifier could be created by following the convention for the Java packages. E.g. com.companyName.domain.opName
<alias>A user-friendly alias name to be used. This name should be easily usable on the command line. So it should be short but still distinguishable. Let's say your operator computes a cloud mask using neural nets. You could name it 'NNBasedCloudMaskOperator', but this is not handy. So maybe just name it 'CompanyNameClouds' or invent some other handy name. You can use the gpt tool to get a list of already existing operators and their names.
<operatorClass>This must be always org.esa.snap.python.gpf.PyOperator

<version>

The version of the operator. It should follow the concept of Semantic Versioning
<authors>The names of the authors

<copyright>

The copyright notice
<description>A short description of the operator shown on the command line
<namedSourceProducts>
<sourceProduct>
<name>myInput</name>
</sourceProduct>
<sourceProduct>
<name>auxProduct</name>

</sourceProduct>

</namedSourceProducts>
This section defines one or more source products. In the GUI currently, one product is supported. On the command line, it is possible to use the names along with the -S option to specify the source product, like -SmyInput=<path>

<parameters>
    <parameter>
      <!--Parameter definiton-->
    <parameter>
<parameters>

In the parameters section, all the parameters needed by the operator are specified. Each parameter is surrounded by the <parameter> tag.

The following table shows the possible tags to configure a parameter (inside the <parameter> tag).

XML TagDescription

<name>

The name of the parameter.
<description>The description of the parameter shown on the command line and as tool-tip in the GUI.
<label>Used in the GUI only instead of the name. The label can have white spaces in contrast to the name.
<unit>
The unit of the parameter. Shown in the user interface.
<dataType>The data type of the parameter can be either byte,char,short,int,long,boolean,double,String.
<defaultValue>The value this parameter shall have by default.
<notNull>The parameter must be provided if set to true.
<notEmpty>The parameter must not be empty if set to true. The difference to <notNull> is that even if the parameter tag present in a graph XML file it is not allowed to be empty. e.g. <bandName></bandName>

<interval>

The valid interval for numeric parameters, e.g. [10,20): in the range 10 (inclusive) to 20 (exclusive)

<valueSet>

Set of values which can be assigned to a parameter field. The value set is given as textual representations of the actual values.

<condition>

A conditional expression which must return true in order to indicate that the parameter value is valid, e.g. value > 2.5

<pattern>

A regular expression pattern to which a textual parameter value must match in order to indicate a valid value, e.g. a*

<format>

A format string to which a textual parameter value must match in order to indicate a valid value, e.g. yyyy-MM-dd HH:mm:ss.Z

<rasterDataNodeClass>

The value can be org.esa.snap.core.datamodel.Band or any other fully qualified name of the sub-classes of RasterDataNode. This ensures that the user can only specify bands, tie-point grids or mask of the source product for this parameter. For example, when the user shall select the bands which shall be processed or the shall select a mask from the input which defines the area to be processed.

Graphical UI for the Operator

Currently, it is not possible to define your own GUI in Python. To use the standard GUI see How to integrate an operator.

Service Registration

Like Java Operators, Python Operators need to be registered via the Java Service Provider Interface (SPI). In order to register the operator, developers need to create a file named 'org.esa.snap.python.gpf.PyOperatorSpi'. This file needs to be placed in the directory resources/META-INF/services in the project structure (see above). This file must contain the python package path and name of the operator. See below for an example and a more detailed explanation.

org.esa.snap.python.gpf.PyOperatorSpi
# In order to publish the python operators in this plugin module to SNAP, they need to be listed in this file. On each
# line an operator class is specified by its fully qualified name.
# A fully specified name includes the Python package path and the class name both separated by dot characters ('.').
# For example:
#
# cloud_mask.CloudMaskOp
# water.mci.MciOp
# land.ndvi.NdviOp
# land_ops.FaparOp
#
# Following Python conventions, the last name in the package path must therefore either be subpackage or a Python
# file. For the 'water.mci.MciOp' entry above, the pysical representation could either be:
# (a)  water/mci.py  where mci.py defines the MciOp class
# (b)  water/mci/__init__.py  where __init__.py defines the MciOp class

ndvi_op.NdviOp

Known issues

  • ensure same Python configuration (used libraries) on the user system as the developer uses
  • Import-Vector can't be used (see forum thread)
  • logging configuration might be changed by snappy (see forum thread)