User Defined Functions (UDF)
While openEO supports a wide range of predefined processes and allows you to build more complex user-defined processes from them, you sometimes need operations or algorithms that are not (yet) available or standardised as openEO processes. User-Defined Functions (UDF) are an openEO feature (via the run_udf process) that aims to fill that gap by allowing a user to express (part of) an algorithm as a Python/R/… script to be run on the back end within the openEO processing environment.
The buttons above let you filter processes supported by different backends. Selecting or deselecting a backend will show or hide the relevant sections in the documentation. However, please note that it is based on the latest documentation rendering. Thus, please refer to the openEO Hub for the most up-to-date information.
Select a UDF execution context
While the above examples show how to define and apply a UDF, it is important to understand the execution context in which the UDF operates. The called process determines the kind of input the UDF receives and the expected output structure.
| Process | UDF input and output contract |
|---|---|
apply |
A per-pixel transformation. Dimensions and labels are preserved. Prefer standard math processes for simple calculations. |
apply_dimension |
A complete series along the chosen dimension; other dimensions can still be chunked. The selected dimension may change in length. |
reduce_dimension |
A complete series along the chosen dimension. The UDF output must remove that dimension. |
apply_neighborhood |
A spatial window. Specify the window size and overlap so that algorithms that require surrounding pixels produce coherent results at chunk boundaries. |
apply_kernel |
A spatial kernel operation. Similar to apply_neighborhood, but typically used for convolution-like operations with a fixed kernel size. |
For example, apply a time-series UDF along the temporal dimension:
smoothed = cube.apply_dimension(process=udf, dimension="t")smoothed <- cube %>% apply_dimension(process = udf, dimension = "t")It is recommended to start with a small batch job to verify the UDF’s behaviour and resource requirements. Additionally, use connection.list_udf_runtimes() to inspect advertised runtimes and libraries.
Logging from UDFs
From time to time, when things are not working as expected, you may want to log additional debug information from your UDF, inspect the data being processed, or log warnings. This can be done using the inspect()function.
For example, to discover the shape of the data cube chunk that you receive in your UDF function:
from openeo.udf import inspect
import xarray
def apply_datacube(cube: xarray.DataArray, context: dict) -> xarray.DataArray:
inspect(data=[cube.shape], message="UDF input chunk shape")
return cube * 0.0001library(openeo)
apply_datacube <- function(cube, context) {
inspect(data = list(dim(cube)), message = "UDF input chunk shape")
return(cube * 0.0001)
}job.logs()job$logs()Backends can limit the types accepted by inspect and may process many chunks, so logging is best used sparingly for diagnosis.
Keep UDFs small and use only packages documented by the selected backend. Standard openEO processes are generally more portable and can be optimised more easily by a backend.
Choose the right extension mechanism
A callback is not a UDF. With apply, apply_dimension, reduce_dimension, aggregate_temporal, aggregate_spatial, or apply_neighborhood, a Python lambda describes an openEO process graph using standardised processes; no arbitrary Python code is sent to the backend. A UDF sends backend-executed Python or R code through run_udf.
| Need | Preferred mechanism |
|---|---|
| Scale values, calculate an index, or reduce a dimension | A standard process or callback. |
| Reuse a named process graph | A UDP. |
| Apply a backend-supported ML model | The backend’s ML process. |
| Run an algorithm unavailable as an openEO process | run_udf. |
| Use a custom external execution environment | run_udf_externally, when advertised by the backend. |
Do not confuse user-defined functions (abbreviated as UDF) with user-defined processes (sometimes abbreviated as UDP) in openEO, which is a way to define and use your own process graphs as reusable building blocks. See User-Defined Processes (UDP) for more information.
Run a backend UDF
Use run_udf when the required algorithm cannot be expressed with standardised openEO processes. This process can either: - load and run a UDF stored in a file on the server-side workspace of the authenticated user. The path to the UDF file must be relative to the root directory of the user’s workspace. - fetch and run a remotely stored and published UDF by absolute URI. - run the source code specified inline as a string.
The loaded UDF can be executed in several processes, such as aggregate_spatial, apply, apply_dimension and reduce_dimension. The user must ensure that the data is provided in a way that the UDF code can interpret.
result = cube.run_udf(udf=udf_code, runtime="Python")result <- cube$run_udf(udf=udf_code, runtime="R")Run an external UDF
Use run_udf_externally when the computation must run in an environment managed outside the backend. This is useful for custom dependencies, but it is less portable and may require backend-specific credentials or configuration.
result = openeo.processes.run_udf_externally(data=cube, udf=udf_code)result <- openeo::run_udf_externally(data=cube, udf=udf_code)Run an OGC API process hosted by the backend
run_ogcapi invokes an OGC API - Processes process that the connected backend offers through its own OGC API - Processes deployment (Part 2: Deploy, Replace, Undeploy). Use it to reuse a process the backend already deployed under OGC API - Processes, instead of re-implementing the same logic as a UDF.
result = openeo.processes.run_ogcapi(data=cube, id="ogc-process-id")result <- openeo::run_ogcapi(data = cube, id = "ogc-process-id")Run an externally hosted OGC API process
run_ogcapi_externally runs an OGC API - Processes process hosted outside the connected backend, either by another service provider or on the user’s own machine. This is useful for calling a process that is only available at a third-party OGC API - Processes endpoint.
result = openeo.processes.run_ogcapi_externally(
data=cube, url="https://example.org/ogcapi", id="ogc-process-id"
)result <- openeo::run_ogcapi_externally(
data = cube, url = "https://example.org/ogcapi", id = "ogc-process-id"
)Both processes are experimental and depend on the backend (or external service) implementing OGC API - Processes; check process support before relying on them in production.
Apply a standardised pixel callback
Apply a standardised process callback independently to each pixel; this is not arbitrary Python execution. This ensures that the operation is applied consistently across the entire data cube.
scaled = cube.apply(lambda value: value * 0.0001)scaled <- cube$apply(function(value) value * 0.0001)Apply a standardised dimension callback
Apply a standardised callback along one cube dimension. You can also find more information about apply_dimension in the spatial and temporal processing sections, which apply the operation along the respective dimensions.
smoothed = cube.apply_dimension(dimension="t", process=lambda series: series.median())smoothed <- cube$apply_dimension(dimension="t", process=function(series) median(series))Reduce a dimension with a UDF
Similar to apply_dimension, but instead of returning a series along the reduced dimension, it collapses the dimension into a single value. The process has also been discussed in the spatial and temporal processing sections to illustrate its usage along those dimensions.
Thus, use reduce_dimension when a UDF must turn every series along one dimension into a single value and remove that dimension from its output.
reduced = cube.reduce_dimension(dimension="t", reducer=udf)reduced <- cube$reduce_dimension(dimension="t", reducer=udf)Apply a standardised neighbourhood callback
The apply_neighborhood process applies a focal process to a data cube.
A focal process operates on a ‘neighbourhood’ of pixels. The neighbourhood can extend into multiple dimensions; the size argument specifies this extent. It is not only (part of) the size of the input window, but also the size of the output for a given position of the sliding window. The sliding window moves in multiples of size.
An overlap can be specified so that neighbourhoods can have overlapping boundaries. This allows for continuity of the output. The overlap region must be included in the data cube or array returned by process, but any changed values will be ignored. The missing overlap at the borders of the original data cube is made available as no-data values in the sub-data cubes.
The neighbourhood size should be kept small enough to avoid exceeding computational resources, but a too-small size will result in more process invocations, which may slow down processing. Window sizes for spatial dimensions typically range from 64 to 512 pixels, while overlaps of 8 to 32 pixels are common.
For the special case of 2D convolution, it is recommended to use apply_kernel.
result = cube.apply_neighborhood(size=[3, 3], process=lambda window: window.mean())result <- cube$apply_neighborhood(size=c(3, 3), process=function(window) mean(window))