Issues in handling data fusion requests

Hi all. For some analysis, I am looking at comparing a Sentinel-1 image during an event (e.g. flooding) with a collection of images preceding the event. Thus I have to get (1) an event image at a given date, and (2) a collection of images over a time range preceding the event (of which I calculate e.g. a backscatter average), and I am comparing the two.

I am trying to approach this by making a Process API data fusion request, both collections being Sentinel-1 but with the corresponding dates or dates ranges. However I am having some issues, with two select examples below:

  • When trying to export outputMetadata (userdata). This work well on a single collection without identifier, but fails in a data fusion with identifiers.
  • When, for instance, computing the average backscatter of a data collection. Works fine again for a sole collection, but also fails on data fusion.

For instance, this works:

  function setup() {
    return {
      input: [
        {bands: ["VV", "dataMask"]},
      ],

      output: [
        {id: "default", bands: 1}
      ],

      mosaicking: "TILE",
    };
  }

  function updateOutputMetadata(scenes, inputMetadata, outputMetadata) {
      outputMetadata.userData = {"scenes": scenes.tiles}
  }

  function calculateAverage(samples) {
    var sum = 0
    var nValid = 0
    for (let sample of samples) {
      if (sample.dataMask != 0) {
        nValid++
        sum += sample.{pol}
      }
    }
    return sum / nValid
  }

  function evaluatePixel(samples) {
    return [calculateAverage(samples)]
  }

But not this (data fusion request with a beforeEvent and afterEvent S1 sources, only showing the code pertaining to beforeEvent):

  function setup() {
    return {
      input: [
        {bands: ["VV", "dataMask"], datasource: "beforeEvent"},
        {bands: ["VV", "dataMask"], datasource: "afterEvent"},
      ],

      output: [
        {id: "default", bands: 1}
      ],

      mosaicking: "TILE",
    };
  }

  function updateOutputMetadata(scenes, inputMetadata, outputMetadata) {
      outputMetadata.userData = {"scenesBeforeEvent": scenes.beforeEvent.tiles}
  }

  function calculateAverage(samples) {
    var sum = 0
    var nValid = 0
    for (let sample of samples) {
      if (sample.dataMask != 0) {
        nValid++
        sum += sample.VV
      }
    }
    return sum / nValid
  }

  function evaluatePixel(samples) {
    return [calculateAverage(samples.beforeEvent)]
  }

When simply trying to get the metadata, I simply get an empty JSON file ({}). When including the code to return the average of the beforeEvent samples, I am getting this:

Server response: "{"error":{"status":400,"reason":"Bad Request","message":"Failed to evaluate script!\nevalscript.js:28: TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined\n for (let sample of samples) {\n ^\nTypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined\n at calculateAverage (evalscript.js:28:24)\n at evaluatePixel (evalscript.js:48:13)\n at executeForMultipleScenes (<anonymous>:1153:14)\n","code":"RENDERER_EXCEPTION"}}"

I am certainly missing something with the data structure of the samples object and how to access them based on their identifiers. Would this kind of processing be possible without a data fusion request?

Hi Marcus,

Would you mind sharing some dates and a bounding box so that we can reproduce your error?

The reason I am asking is because I can run you script snippet without a problem for a test area in the UK.

For the following question:

When simply trying to get the metadata, I simply get an empty JSON file ({} ).

If you replace:

outputMetadata.userData = {"scenesBeforeEvent": scenes.beforeEvent.tiles}

by

outputMetadata.userData = {"scenesBeforeEvent": scenes.beforeEvent.scenes.tiles}

you should be able to write information to your JSON. With datafusion, you access your datasource’s scenes with the nomenclature scenes.<datasource_name>.scenes.

Hi Maxim, thanks for getting back to me. Turns out part of the problem was a keyboard-chair interface issue :sweat_smile: - when debugging I disabled the code that fetches the second data source (afterEvent). While I took care of not referencing a source I do not request, it seems the API does not accept processing single sources that have an assigned user ID (e.g. beforeEvent), when there is no other source requested. So, it is working well with both beforeEvent and afterEvent being called in the script!

Getting the metadata through scenes.{sourceID}.scenes.tiles (or I assume .orbits depending on the mosaicking) works well too. Thank you.

I just have a second issue I allow myself to piggyback with on this post regarding relative orbit filtering. I have been trying to replicate using code from the example here, adapting it for S1 sats. The modified orbit filtering code snipper is the following (changing the satellite names and the rel. orb. coefficients):


  //VERSION=3

  function setup() {
    return {
      input: [
        {bands: ["VV", "dataMask"], datasource: "beforeEvent"},
        {bands: ["VV", "dataMask"], datasource: "afterEvent"}
      ],
      output: [
        {{id: "mask", bands: 1}}
      ],
      mosaicking: "TILE",
    };
  }

  var orbNr = 147 // or 74 for the given test dates

  function getAbsOrbitIdFromTileOriginalId(tileOriginalId) {
    textParts = tileOriginalId.split("_")
    absOrbitId = parseInt(textParts[6].substring(1));
    return absOrbitId
  }

  function getSatelliteFromTileOriginalId(tileOriginalId) {
    textParts = tileOriginalId.split("_")
    satellite = textParts[0];
    return satellite
  }

  function getRelativeOrbitIdFromAbsOrbitId(absOrbitId, satellite) {
    relativeOrbitCoefficinets = {
        // Arrays of coefficients [firstRelOrbit, maxRelOrbit, add], where:
        // Relative Orbit Number = mod (Absolute Orbit Number orbit + firstRelOrbit, maxRelOrbit) + add
        "S1A": [-73, 175, 1],
        "S1B": [-27, 175, 1],
      }

    coefficients = relativeOrbitCoefficinets[satellite.toString()]
    return (absOrbitId + coefficients[0]) % coefficients[1] + coefficients[2]
  }

  // Filter by relative orbit id
  function preProcessScenes(collections) {
    var allowedRelativeOrbits = [orbNr]
    collections.scenes.tiles = collections.scenes.tiles.filter(function(tile) {
      var satellite = getSatelliteFromTileOriginalId(tile.tileOriginalId);
      var absOrbitId = getAbsOrbitIdFromTileOriginalId(tile.tileOriginalId);
      return allowedRelativeOrbits.includes(getRelativeOrbitIdFromAbsOrbitId(absOrbitId, satellite))
    })
    return collections;
  }

  function evaluatePixel(samples) {{
    return [samples.afterEvent] // just returning this for testing
  }

My test BBox is the following:
BBox(((153.22406171417035, -28.87289144461865), (153.39147095497796, -28.742148861725493)), crs=CRS('4326'))

of size (546, 481).

My beforeEvent date range is 2022-01-01 - 2022-01-31, and my afterEvent images are:

  • 2022-03-26, orbit number 74
  • 2022-03-31, orbit number 147

Using on these dates in this area throws the error:
Server response: "{"error":{"status":400,"reason":"Bad Request","message":"Failed to evaluate script!\nevalscript.js:46: TypeError: Cannot read property 'tiles' of undefined\n collections.scenes.tiles = collections.scenes.tiles.filter(function(tile) {\n ^\nTypeError: Cannot read property 'tiles' of undefined\n at preProcessScenes (evalscript.js:46:50)\n at internalPreProcessScenes (<anonymous>:1187:11)\n","code":"RENDERER_EXCEPTION"}}"

I would assume the filtering code filters for both collections.scenes.beforeEvent.scenes.tiles and collections.scenes.afterEvent.scenes.tiles, but following your statement on accessing metadata for each collection, I am not sure now. How should I modify the preProcessScenes() function?

Thanks again for your help, and have a good evening!

Glad to hear that you solved the first issue :slight_smile:

I would also like to thank you for providing all the information about your problem: it makes it so much easier for me to help you :wink:

In a similar fashion to the use of scenes in the outputMetadata function, you can access the collection tiles in preProcessScenes by doing:

collections.beforeEvent.scenes.tiles

Do you really need to filter for the given orbit for afterEvent since you are using a single date? I would just filter the beforeEvent scenes, using the following function:

function preProcessScenes(collections) {

    var allowedRelativeOrbits = [orbNr]
    collections.beforeEvent.scenes.tiles = collections.beforeEvent.scenes.tiles.filter(function(tile) {
      var satellite = getSatelliteFromTileOriginalId(tile.tileOriginalId);
      var absOrbitId = getAbsOrbitIdFromTileOriginalId(tile.tileOriginalId);
      return allowedRelativeOrbits.includes(getRelativeOrbitIdFromAbsOrbitId(absOrbitId, satellite))
    })
    return collections;
  }

If you want to make the code more generic and also filter the afterEvent scenes (maybe in the case where you would set a time-range and leave the orbit number as a variable in your code), then you could just add a second filter function in the preProcessScenes for collections.afterEvent.scenes.tiles .

Hope this helps!

No worries, it’s better when an issue is somewhat documented :smiley:

The code is running but it’s not yet coming together for me :sweat_smile: . Indeed my outputs seem to show that both relative orbits present in the area are fetched, although I updated my preProcessScenes() function as you suggested.

Where it gets interesting is that the metadata shows that the scenes.beforeEvent and scenes.afterEvent were filtered, but the collections apparently still contains tiles for both relative orbits.

My output metadata looks like the following. You can see that collections, which should have been filtered, comprises more images than the filtered scenes (indexes 0 to 5 for collections.beforeEvent thus apparently unfiltered, whereas beforeEvent scenes contains only 2 images, as correctly filtered by rel. orb.).

{
	"collections": {
		"beforeEvent": {
			"scenes": [
				{
					"__idx": 0
				},
				{
					"__idx": 1
				},
				{
					"__idx": 2
				},
				{
					"__idx": 3
				},
				{
					"__idx": 4
				},
				{
					"__idx": 5
				}
			]
		},
		"afterEvent": {
			"scenes": [
				{
					"__idx": 0
				}
			]
		}
	},
	"beforeEvent": [
		{
			"date": "2022-01-30T19:14:20Z",
			"shId": 2594357,
			"tileOriginalId": "S1A_IW_GRDH_1SDV_20220130T191420_20220130T191445_041694_04F5F9_B306",
			"__idx": 0,
			"dataPath": "s3://sentinel-s1-l1c/GRD/2022/1/30/IW/DV/S1A_IW_GRDH_1SDV_20220130T191420_20220130T191445_041694_04F5F9_B306"
		},
		{
			"date": "2022-01-06T19:14:21Z",
			"shId": 2579496,
			"tileOriginalId": "S1A_IW_GRDH_1SDV_20220106T191421_20220106T191446_041344_04EA48_80A7",
			"__idx": 1,
			"dataPath": "s3://sentinel-s1-l1c/GRD/2022/1/6/IW/DV/S1A_IW_GRDH_1SDV_20220106T191421_20220106T191446_041344_04EA48_80A7"
		}
	],
	"afterEvent": [
		{
			"date": "2022-03-31T19:14:20Z",
			"shId": 2634363,
			"tileOriginalId": "S1A_IW_GRDH_1SDV_20220331T191420_20220331T191445_042569_0513ED_F4D8",
			"__idx": 0,
			"dataPath": "s3://sentinel-s1-l1c/GRD/2022/3/31/IW/DV/S1A_IW_GRDH_1SDV_20220331T191420_20220331T191445_042569_0513ED_F4D8"
		}
	]
}

If you update my code snippet provided previously with those modified functions, you should be able to reproduce (on the same BBox and dates, for orbit 147: 2022-03-31, previous dates 01-01 to 01-30):

  function setup() {
    return {
      input: [
        {{bands: ["VV", "dataMask"], datasource: "beforeEvent"}},
        {{bands: ["VV", "dataMask"], datasource: "afterEvent"}}
      ],
      output: [
        {id: "preEvent", bands: 1},
        {id: "postEvent", bands: 1},
      ],
      mosaicking: "TILE",
    };
  }

  var colMetadata
  var orbNr = 147

  function preProcessScenes(collections) {
    var allowedRelativeOrbits = [orbNr]
    collections.beforeEvent.scenes.tiles = collections.beforeEvent.scenes.tiles.filter(function(tile) {{
      var satellite = getSatelliteFromTileOriginalId(tile.tileOriginalId);
      var absOrbitId = getAbsOrbitIdFromTileOriginalId(tile.tileOriginalId);
      return allowedRelativeOrbits.includes(getRelativeOrbitIdFromAbsOrbitId(absOrbitId, satellite))
    })
    collections.afterEvent.scenes.tiles = collections.afterEvent.scenes.tiles.filter(function(tile) {
      var satellite = getSatelliteFromTileOriginalId(tile.tileOriginalId);
      var absOrbitId = getAbsOrbitIdFromTileOriginalId(tile.tileOriginalId);
      return allowedRelativeOrbits.includes(getRelativeOrbitIdFromAbsOrbitId(absOrbitId, satellite))
    })
    colMetadata = collections
    return collections
  }

  function updateOutputMetadata(scenes, inputMetadata, outputMetadata) {
      outputMetadata.userData = {
        "beforeEvent": scenes.beforeEvent.scenes.tiles,
        "afterEvent": scenes.afterEvent.scenes.tiles,
        "collections": colMetadata
        }
  }

  function evaluatePixel(samples) {
    var beforeEventAverage = calculateAverage(samples.beforeEvent)
    // averaging should be unnecessary since it should be only 1 image, but keeping here in case.
    var afterEvent = calculateAverage(samples.afterEvent)

    return {
      postEvent: [afterEvent],
      preEvent: [beforeEventAverage]
    } 
  }

Which outputs the previously pasted metadata, and the following TIF for the preEvent response:
image

What am I missing to pass the filtered tiles to evaluatePixels()? Thank you again for your help, and apologies for not getting there yet.

Hi Marcus,

Sorry it took so long…

What I didn’t notice at first in your script is that you are using mosaicking TILE parameter. Sentinel-1 GRD doesn’t support TILE mosaicking: see the documentation. You will need to switch the Evalscript to ORBIT: (https://docs.sentinel-hub.com/api/latest/evalscript/v3/#mosaicking), but a few modifications are needed.

The preProcessScenes won’t be able to fetch the tile IDs anymore, but you can still access them in the evaluatePixel function. E.g.:

scenes.beforeEvent.scenes.orbits[0].tiles

I would recommend you do the filtering in the evaluatePixel function by only pushing VV values to an array if the tiles of your orbit are the relative orbit you need, then averaging this array.

Thanks for the tip Maxim. Switched back to ORBIT. Here’s my implementation so far in evaluatePixel(), which seems to work at a first glance. I still need to make it loop through the tiles contained in orbits[i] if multiple relative orbits are present in a single day (now I assume there is only one tile, tiles[0]), but this is an edge case for now. I’m open to any suggestion on how to improve it.

allowedRedOrbs = [...]

// only showing filtering for beforeScenes here
beforeEventSamples = []
beforeEventScenesLength = scenes.beforeEvent.scenes.orbits.length

for (var i = 0; i < beforeEventScenesLength; i++) {
  beforeSceneId = scenes.beforeEvent.scenes.orbits[i].tiles[0].tileOriginalId
  beforeSceneSat = "S1A" // since only S1A is operating now for my period of interest

  // see previous post or SHub examples for these functions
  beforeSceneRelOrb = getRelativeOrbitIdFromAbsOrbitId(getAbsOrbitIdFromTileOriginalId(beforeSceneId), beforeSceneSat)
  if (allowedRedOrbs.includes(beforeSceneRelOrb)){
    beforeEventSamples.push(samples.beforeEvent[i])}
}

var beforeEventAverage = calculateAverage(beforeEventSamples)

(calculateAverage() then takes care of picking the right polarisation)