Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
[
{
"id": "s2_sam",
"type": "openeo",
"description": "Run Sentinel-2 based Spectral Angle Mapping (S2-SAM) on a monthly BAP composite and return the best matching USGS spectral library class index per pixel.",
"backend": "openeofed.dataspace.copernicus.eu",
"process_graph": {
"s2sam1": {
"arguments": {
"spatial_extent": {
"geometry": {
"coordinates": [
[
[
-15.487987,
27.813076
],
[
-15.489753,
27.814047
],
[
-15.493856,
27.809741
],
[
-15.492555,
27.808545
],
[
-15.487809,
27.806311
],
[
-15.485954,
27.809176
],
[
-15.484348,
27.812444
],
[
-15.48635,
27.81229
],
[
-15.487987,
27.813076
]
]
],
"type": "Polygon"
},
"properties": {
"CNTR_ID": "ES",
"ID": "598"
},
"type": "Feature"
},
"temporal_extent": [
"2024-05-01",
"2024-05-31"
]
},
"namespace": "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/c0f8d549a866737a3c276f9838fa0a019e7975be/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json",
"process_id": "s2_sam"
},
"saveresult1": {
"arguments": {
"data": {
"from_node": "s2sam1"
},
"format": "NetCDF",
"options": {}
},
"process_id": "save_result",
"result": true
}
},
"reference_data": {
"job-results.json": "https://s3.waw3-1.cloudferro.com/apex-benchmarks-internal/HP120!tests_test_benchmarks.py__test_run_benchmark_s2_sam_!actual/job-results.json",
"openEO.nc": "https://s3.waw3-1.cloudferro.com/apex-benchmarks-internal/HP120!tests_test_benchmarks.py__test_run_benchmark_s2_sam_!actual/openEO.nc"
}
}
]
156 changes: 156 additions & 0 deletions algorithm_catalog/terravision/s2_sam/notebook/s2_sam.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "31162cad",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Authenticated using refresh token.\n"
]
}
],
"source": [
"import openeo\n",
"\n",
"connection = openeo.connect(\"openeofed.dataspace.copernicus.eu\").authenticate_oidc()"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "26bc37db",
"metadata": {},
"outputs": [],
"source": [
"spatial_extent = {\n",
" \"type\": \"Feature\",\n",
" \"properties\": {\n",
" \"ID\": \"598\",\n",
" \"CNTR_ID\": \"ES\"\n",
" },\n",
" \"geometry\": {\n",
" \"type\": \"Polygon\",\n",
" \"coordinates\": [[\n",
" [-15.487987, 27.813076],\n",
" [-15.489753, 27.814047],\n",
" [-15.493856, 27.809741],\n",
" [-15.492555, 27.808545],\n",
" [-15.487809, 27.806311],\n",
" [-15.485954, 27.809176],\n",
" [-15.484348, 27.812444],\n",
" [-15.486350, 27.812290],\n",
" [-15.487987, 27.813076]\n",
" ]]\n",
" }\n",
"}\n",
"temporal_extent = [\"2024-05-01\", \"2024-05-31\"]"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "9fb09937",
"metadata": {},
"outputs": [],
"source": [
"public_url = \"https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/c0f8d549a866737a3c276f9838fa0a019e7975be/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json\"\n",
"sam_cube = connection.datacube_from_process(\n",
" \"s2_sam\", \n",
" namespace = public_url,\n",
" temporal_extent = temporal_extent,\n",
" spatial_extent= spatial_extent\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "4918618a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0:00:00 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': send 'start'\n",
"0:00:05 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': queued (progress 0%)\n",
"0:00:10 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': queued (progress 0%)\n",
"0:00:17 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': queued (progress 0%)\n",
"0:00:26 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': queued (progress 0%)\n",
"0:00:36 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': queued (progress 0%)\n",
"0:00:48 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': queued (progress 0%)\n",
"0:01:04 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': running (progress N/A)\n",
"0:01:23 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': running (progress N/A)\n",
"0:01:47 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': running (progress N/A)\n",
"0:02:17 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': running (progress N/A)\n",
"0:02:54 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': running (progress N/A)\n",
"0:03:41 Job 'cdse-j-2608111342194bf0ac97d86c30fb35e5': finished (progress 100%)\n"
]
},
{
"data": {
"text/html": [
"\n",
" <script>\n",
" if (!window.customElements || !window.customElements.get('openeo-job')) {\n",
" var el = document.createElement('script');\n",
" el.src = \"https://cdn.jsdelivr.net/npm/@openeo/vue-components@2/assets/openeo.min.js\";\n",
" document.head.appendChild(el);\n",
"\n",
" var font = document.createElement('font');\n",
" font.as = \"font\";\n",
" font.type = \"font/woff2\";\n",
" font.crossOrigin = true;\n",
" font.href = \"https://use.fontawesome.com/releases/v5.13.0/webfonts/fa-solid-900.woff2\";\n",
" document.head.appendChild(font);\n",
" }\n",
" </script>\n",
" <openeo-job>\n",
" <script type=\"application/json\">{\"currency\": \"credits\", \"federation\": {\"cdse\": {\"description\": \"This openEO service runs on the [Copernicus Data Space Ecosystem](https://dataspace.copernicus.eu/)\\nand offers data access and processing on full archives of Copernicus data, such as the Sentinels.\", \"last_status_check\": \"2026-08-11T13:41:16Z\", \"status\": \"online\", \"title\": \"Copernicus Data Space Ecosystem openEO API\", \"url\": \"https://openeo.dataspace.copernicus.eu/openeo/1.1/\"}, \"terrascope\": {\"description\": \"OpenEO API to the Terrascope/VITO Remote Sensing product catalog and processing services (using GeoPySpark driver).\", \"last_status_check\": \"2026-08-11T13:41:16Z\", \"status\": \"online\", \"title\": \"Terrascope openEO API\", \"url\": \"https://openeo.terrascope.be/openeo/1.1/\"}}, \"job\": {\"costs\": 4, \"created\": \"2026-08-11T13:42:19Z\", \"id\": \"cdse-j-2608111342194bf0ac97d86c30fb35e5\", \"process\": {\"process_graph\": {\"s2sam1\": {\"arguments\": {\"spatial_extent\": {\"geometry\": {\"coordinates\": [[[-15.487987, 27.813076], [-15.489753, 27.814047], [-15.493856, 27.809741], [-15.492555, 27.808545], [-15.487809, 27.806311], [-15.485954, 27.809176], [-15.484348, 27.812444], [-15.48635, 27.81229], [-15.487987, 27.813076]]], \"type\": \"Polygon\"}, \"properties\": {\"CNTR_ID\": \"ES\", \"ID\": \"598\"}, \"type\": \"Feature\"}, \"temporal_extent\": [\"2024-05-01\", \"2024-05-31\"]}, \"namespace\": \"https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/c0f8d549a866737a3c276f9838fa0a019e7975be/algorithm_catalog/terravision/s2_sam/openeo_udp/s2_sam.json\", \"process_id\": \"s2_sam\"}, \"saveresult1\": {\"arguments\": {\"data\": {\"from_node\": \"s2sam1\"}, \"format\": \"NetCDF\", \"options\": {}}, \"process_id\": \"save_result\", \"result\": true}}}, \"progress\": 100, \"status\": \"finished\", \"title\": \"S2 SAM processing for 2024-05-01 to 2024-05-31\", \"updated\": \"2026-08-11T13:45:18Z\", \"usage\": {\"cpu\": {\"unit\": \"cpu-seconds\", \"value\": 162}, \"duration\": {\"unit\": \"seconds\", \"value\": 115}, \"input_pixel\": {\"unit\": \"mega-pixel\", \"value\": 1.25}, \"max_executor_memory\": {\"unit\": \"gb\", \"value\": 2.0545730590820312}, \"memory\": {\"unit\": \"mb-seconds\", \"value\": 1290240}, \"memory_requested\": {\"unit\": \"mb-seconds\", \"value\": 1290240}, \"network_received\": {\"unit\": \"b\", \"value\": 364352977}}}}</script>\n",
" </openeo-job>\n",
" "
],
"text/plain": [
"<BatchJob job_id='cdse-j-2608111342194bf0ac97d86c30fb35e5'>"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"job = sam_cube.create_job(\n",
" title=f'S2 SAM processing for {temporal_extent[0]} to {temporal_extent[1]}',\n",
" out_format=\"NetCDF\")\n",
"job.start_and_wait()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "cdse",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
50 changes: 50 additions & 0 deletions algorithm_catalog/terravision/s2_sam/openeo_udp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Sentinel-2 based Spectral Angle Mapping (S2-SAM)

This service applies Spectral Angle Mapping (SAM) to Sentinel-2 Level-2A reflectance data.
For each pixel, the algorithm compares the pixel spectrum to a spectral reference library and selects the closest match based on the smallest spectral angle.

## What this service does

SAM measures similarity in spectral *shape* (not absolute brightness):

$$
θ = \cos^{-1}\left(\frac{x \cdot y}{\|x\|\|y\|}\right)
$$

where:
- $x$ is the pixel spectrum.
- $y$ is a reference spectrum from the library.
- smaller $\theta$ means a better match.

The output is a raster of class indices, where each pixel value is the index of the best-matching reference signature.

## Sentinel-2 bands used

The service uses only the Sentinel-2 L2A bands provided by the BAP composite input, not all L2A layers.
The band subset is (B01, B02, B03, B04, B05, B06, B07, B08, B8A, B09, B11, B12).

Input compositing is based on the BAP process:
[https://algorithm-catalogue.apex.esa.int/apps/bap_composite](https://algorithm-catalogue.apex.esa.int/apps/bap_composite)

## Reference spectral library

The reference library is based on a Sentinel-2-resampled USGS mineral/material collection.
It includes a broad set of geology and mining-related targets (e.g. iron oxides, clays, sulfates, silicates, carbonates, and related materials).

The CSV used by the service contains:
- 498 named reference signatures (material columns).
- 1 `cwvl` column with central wavelength values.

Examples of included targets are minerals such as Acmite, Actinolite, Alunite, Jarosite, Kaolinite, Hematite, Goethite, Pyrite, Chalcopyrite, and many more variants.

## Notes on interpretation

- The product reports the **closest spectral match** in the selected library, not a definitive mineral identification.
- Similar materials can produce similar spectra at Sentinel-2 spectral resolution.
- Results are most useful when combined with geological context, ancillary data, and expert validation.

## Missing data handling

If some bands are missing (`NaN`) at a pixel, the implementation computes SAM using only the available bands, with a minimum valid-band threshold.
Pixels without enough valid information are marked as no match.

52 changes: 52 additions & 0 deletions algorithm_catalog/terravision/s2_sam/openeo_udp/generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from datetime import date
import json
from pathlib import Path

import openeo
from openeo.api.process import Parameter
from openeo.rest.udp import build_process_dict

def generate():
connection = openeo.connect("openeofed.dataspace.copernicus.eu").authenticate_oidc()

spatial_extent = Parameter.spatial_extent(
name="spatial_extent",
description="Limits the data to process to the specified bounding box or polygons.\\n\\nFor raster data, the process loads the pixel into the data cube if the point at the pixel center intersects with the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC).\\nFor vector data, the process loads the geometry into the data cube if the geometry is fully within the bounding box or any of the polygons (as defined in the Simple Features standard by the OGC). Empty geometries may only be in the data cube if no spatial extent has been provided.\\n\\nEmpty geometries are ignored.\\nSet this parameter to null to set no limit for the spatial extent."
)

temporal_extent = Parameter.temporal_interval(
name="temporal_extent",
description="Temporal extent specified as two-element array with start and end date/date-time."
)


bap_cube = connection.datacube_from_process(
"bap_composite",
namespace = "https://raw.githubusercontent.com/ESA-APEx/apex_algorithms/refs/heads/main/algorithm_catalog/vito/bap_composite/openeo_udp/bap_composite.json",
temporal_extent = temporal_extent,
geometry= spatial_extent,
bands = ['B01','B02','B03','B04','B05','B06','B07','B08','B8A','B09','B11','B12'],
max_cloud_cover = 80
)
composite = bap_cube.aggregate_temporal_period("month","first")

udf = openeo.UDF.from_file(
Path(__file__).parent / "sam_udf.py",
)
processed_cube = composite.apply(process=udf)


return build_process_dict(
process_graph=processed_cube,
process_id="s2_sam",
description=(Path(__file__).parent / "README.md").read_text(),
parameters=[
spatial_extent,
temporal_extent,
]
)


if __name__ == "__main__":
with open("s2_sam.json", "w") as f:
json.dump(generate(), f, indent=2)
Loading
Loading