NumPy API

The Workflows Array type mimics a NumPy ndarray, supporting vectorized operations, broadcasting, and advanced indexing with the same syntax as NumPy. Workflows also contains a workflows.numpy submodule with equivalent versions of over 175 NumPy routines.

You can access a Workflows Array from Image.ndarray and ImageCollection.ndarray. You can also construct one from a local NumPy array or list, as long as it’s relatively small (< 10MiB when JSON-serialized as a list):

>>> import descarteslabs.workflows as wf
>>> import numpy as np
>>> np_arr = np.array([[1.1, 2.2, 4.4], [8.8, 9.9, 10.0]])
>>> wf.Array.from_numpy(np_arr)
<descarteslabs.workflows.types.array.array_.Array at 0x...>

>>> imgs = wf.ImageCollection.from_id("sentinel-2:L1C", start_datetime="2018-01-01", end_datetime="2018-03-01")
>>> imgs.ndarray
<descarteslabs.workflows.types.array.masked_array.MaskedArray at 0x...>

>>> arr = wf.Array([[1, 2, 4], [8, 9, 10]])
>>> arr
<descarteslabs.workflows.types.array.array_.Array at 0x...>

Slicing

Arrays support the most of the indexing syntax from NumPy:

>>> arr = wf.Array([[1, 2, 4],
...                 [8, 9, 10]])

>>> arr[0]
<descarteslabs.workflows.types.array.array_.Array at 0x...>
>>> arr[0].compute()
array([1, 2, 4])

>>> arr[0, 0]
<descarteslabs.workflows.types.primitives.number.Int at 0x...>
>>> arr[0, 0].compute()
1

>>> arr[:, [0, 2]]
<descarteslabs.workflows.types.array.array_.Array at 0x...>
>>> arr[:, [0, 2]].compute()
array([[ 1,  4],
       [ 8, 10]])

>>> arr[np.newaxis, 0, 1].compute()
<descarteslabs.workflows.types.array.array_.Array at 0x...>
>>> arr[np.newaxis, 0, 1].compute()
array([2])

>>> arr[..., 0]
<descarteslabs.workflows.types.array.array_.Array at 0x...>
>>> arr[..., 0].compute()
array([1, 8])


>>> (arr > 3)
<descarteslabs.workflows.types.array.array_.Array at 0x...>
>>> (arr > 3).compute()
array([[False, False,  True],
       [ True,  True,  True]])

>>> arr[arr > 3]
<descarteslabs.workflows.types.array.array_.Array at 0x...>
>>> arr[arr > 3].compute()
array([ 4,  8,  9, 10])

Array supports:

  • Slicing by integers and slices: x[0, :5]

  • Slicing by lists/arrays of integers: x[[1, 2, 4]]

  • Slicing by lists/arrays of booleans: x[[False, True, True, False, True]]

  • Slicing one Array with an Array of bools: x[x > 0]

  • Slicing one Array with a zero or one-dimensional Array of ints: a[b]

The only unsupported indexing operations are:

  • Lists or arrays in multiple axes (x[[1, 2, 3], [3, 2, 1]])

  • Slicing with a multi-dimensional Array of ints

Operations and ufuncs

The workflows.numpy submodule contains equivalents of most of the NumPy ufuncs (elementwise numerical operators like np.add, np.sqrt, etc.), and many other routines, including parts of submodules like np.linalg and np.ma. You can use them on proxy types (Array, Int, Float, Bool) as well as NumPy arrays and Python scalars (int, float). They always return proxy types.

Additionally, you can use the actual NumPy version of any of these on a Workflows Array; internally, NumPy will just dispatch to the Workflows version:

>>> import numpy as np
>>> arr = wf.Array.from_numpy(np.arange(4))

>>> wf.numpy.square(arr)
<descarteslabs.workflows.types.array.array_.Array at 0x7ff153cfead0>
>>> wf.numpy.square(arr).compute()
array([0, 1, 4, 9])

>>> np.square(arr)  # still returns Workflows array, even though using the NumPy function
<descarteslabs.workflows.types.array.array_.Array at 0x7ff153cfead0>
>>> np.square(arr).compute()
array([0, 1, 4, 9])

Interacting with Imagery

Arrays and raster objects (Image and ImageCollection) are not directly interoperable (arr + img won’t work, for example).

However, you can access the array from a raster object with the ndarray field. And you can turn an Array back into an Image or ImageCollection with Array.to_imagery. Note that Array.to_imagery always returns an ImageCollection even if the Array is only 3D. If you are expecting an Image, you can index into the result like my_col[0]:

>>> imgs = wf.ImageCollection.from_id("sentinel-2:L1C", start_datetime="2018-01-01", end_datetime="2018-03-01")
>>> rgb = imgs.pick_bands("red green blue")

>>> spectral_target = [0.2, 0.3, 0.4]  # per-band spectral targets
>>> spectral_target_arr = wf.Array(spectral_target)

>>> delta = rgb.ndarray - spectral_target[None, :, None, None]
>>> # ^ must expand to 4 dimensions to align with the ImageCollection's 4D Array

>>> delta_std = delta.std(axis=0)  # std deviation over axis 0, aka `axis="images"`

>>> delta_img = delta_std.to_imagery()[0]  # no properties/bandinfo given
>>> delta_img.compute(wf.map.geocontext())
ImageResult:
* ndarray: MaskedArray<shape=(3, 135, 398), dtype=float64>
* properties:
* bandinfo: 'band_1', 'band_2', 'band_3'
* geocontext: 'geometry', 'resolution', 'crs', 'bounds', ...

Array.to_imagery will create empty metadata for the raster object if you don’t pass any in, defaulting to empty properties and bands named band_1 through band_N. But when appropriate, you can pass in specific metadata:

>>> delta_img_with_metadata = delta_std.to_imagery({"foo": "bar"}, {"red_d": {}, "green_d": {}, "blue_d": {}})[0]
>>> delta_img_with_metadata.compute(wf.map.geocontext())
ImageResult:
  * ndarray: MaskedArray<shape=(3, 135, 398), dtype=float64>
  * properties: 'foo'
  * bandinfo: 'red_d', 'green_d', 'blue_d'
  * geocontext: 'geometry', 'resolution', 'crs', 'bounds', ...