L1C_COLLECTION_URL = "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-2-l1c"
STAC_ROOT_URL = "https://stac.dataspace.copernicus.eu/v1"
w, s, e, n = 11.0, 44.5, 11.1, 44.6
spatial_extent = { "west": w, "south": s, "east": e, "north": n}
# ATTENTION: Inclusion of the second date in the search depends on the query method.
temporal_extent=["2026-04-17", "2026-04-27"]FORCE level 2
Recap
Level 2 processing
The openeo Process is straightforwardly named force_level2. It takes most of the parameters that can be specified in the FORCE level 2 parameter file. See the article on parametrization for more details which parameters are (not) exposed by the process.
The FORCE parameter file format uses SCREAMING_SNAKE_CASE to name its parameters. The integrated FORCE uses (lowercase) snake_case instead, to be consistent with openEO conventions. Otherwise, the parameter names are unchanged, so it is straightforward to determine the openEO name if you are familiar with the FORCE parameters.
The parameters are documented in the process description. We can inspect it using the Python client’s describe_processes().
Authenticated using refresh token.
Authenticated using refresh token.
Authenticated using refresh token.
Building the Process Graph
The FORCE process graph is based on the force_level2 process, as described in openEO processes. Because FORCE does not use the standard openEO version of the “data cube” concept, but uses its own data cube representation, we need to wrap the process so that we can interact with the results in a convenient way as a StacResource and export the processing results to a workspace.
| Concept | Qualified name (Python client) | Purpose |
|---|---|---|
StacResource |
openeo.rest.stac_resource.StacResource |
A handle to the process graph results, makes it possible to inspect the results as usual for openEO datacubes. |
export_workspace |
openeo.rest.result.SaveResult.export_workspace |
A process to publish the processing results (e.g., a FORCE ARD data cube) to a shared cloud storage location. There is a special workspace configured for FORCE (apex-force-force-results) accessible to all users of the CDSE openEO backend. The workspace serves as a storage location to keep data cubes for later processing (e.g., with the TSA module). |
PGNode |
openeo.internal.graph_building.PGNode |
A single node in a process graph. PGNode is an internal class but may be used for advanced graph building purposes as described here. We use it for the graph parameter of StacResource. |
Workspaces
OpenEO uses the concept of Workspaces to interface with object storage. For processing with FORCE, we can use a workspace dedicated to FORCE to store and exchange processing results between level 2 and higher level processing, and with colleagues.
We can use either the dedicated workspace apex-force-results-workspace, which is accessible to all CDSE openeo users, or another registered workspace. See the openEO platform documentation on workspaces on how workspaces can be accessed and registered.
When running FORCE level2 on its own, we do not need to use a workspace. Simply download the results when you are done with processing. However, to reference the level 2 processing results in further processing, we need to use a workspace.
We can instruct openEO to store our results in a workspace using the export_workspace openEO process.
When using export_workspace, we need to pass a merge string to identify your result. The merge will be a key prefix on object storage. The merge is something similar to a path in a file system. It is very important to set a good merge parameter and remember the result, because results can only be accessed when the merge is known. Furthermore, all users of the workspace (in the case of the apex-force-results-workspace, anyone) may write data, so make sure to use a unique merge, to reduce the likelihood of accidentally modifying others’ data and have others overwrite your data by accident.
The dedicated workspace for FORCE results is accessible to all users:
- id:
apex-force-results-workspace - url: https://s3.waw4-1.cloudferro.com/apex-force-results-waw4-1-exotc5yuexi2c5tvwqhoivj62fz8v0uupy0me
We generate the force_level2 process graph using a Process Graph Node (PGNode). In order to interpret the backend response, we wrap the resulting process graph as a StacResource. This makes it possible to render the backend response nicely in Jupyter environments and gives us convenient methods to access our results when the process is done.
Pass parameters to the level 2 processing system in the arguments dictionary. You may give the processing a name which will determine the name of the STAC item generated for the results using the parameter name.
Area of extent
Another parameter which is handled in a special way (i.e., not passed directly to FORCE) is aoi. In FORCE level 2, you can specify the FILE_AOI parameter to clip the output to a certain extent. Instead of a file, aoi takes a geojson object to specify area of interest.
- The
aoiparameter controls the area of the output produced by FORCE. Thespatial_extentparameter passed to the query limits the input files aoiis applied after radiometric correction (so it will not reduce the computational effort) and before tiling as per the FORCE documentation (description of parameterFILE_AOI)- By default, the output will cover all of the inputs, without clipping.
When using the query_stac process, we pass the contents of a STAC document to the process (not a reference). To do this, pass the process_graph directly as stac_document
Due to backend limitations, the stac_document parameter can only handle a document of limited length. The query_stac process generates an item collection which quickly grows in size (the entire content of each STAC item is contained in it). Therefore, use query_stac only for small processing tasks with only a handful of items. Otherwise use a different query method to pass an URL or a catalog instead.
It is recommended to rely on Pystac client for catalog queries for now.
from openeo.rest.stac_resource import StacResource
from openeo.internal.graph_building import PGNode
processing_name = "Modena"
force_l2_stac_resource = StacResource(
graph=PGNode(
process_id="force_level2",
arguments={
"stac_document": query_pg,
"name": processing_name,
"aoi": aoi,
"do_brdf": True,
}
),
connection=connection,
)
# Optional when only processing level 2
# If you intend to continue processing, or share the results with others,
# add the `export_workspace` call:
import getpass
# Be careful: The merge determines the storage location of your results.
# You may overwrite your results or those of others, if the `merge` parameter is not
# Set to a unique value.
merge = f"{getpass.getuser()}/experiments/{processing_name}"
print("merge: ", merge)
force_l2_stac_resource = force_l2_stac_resource.export_workspace(
workspace="apex-force-results-workspace",
merge=merge,
)merge: hannes/experiments/Modena
Due to backend limitations the stac_document parameter can only handle a document of limited length. This means it can only handle collections of very few items. It is therefore advisable to either pass an URL (if possible) or to convert the item_collection returned by pystac_client.Client.search to a catalog. The item collection contains the entire content of each STAC item, whereas the catalog only contains a reference to the items, making a much shorter document.
In case the backend limitation will be fixed, the transformation step will no longer be necessary. It is already possible to pass item_collection.to_dict() directly as stac_document, if the item collection is small.
Convert Item Collection to Catalog
import pystac
def transform_item_collection_to_catalog_with_links(item_collection):
catalog = pystac.Catalog(
id="item-collection-catalog",
description="Catalog from item collection",
)
for item in item_collection.items:
item_link = pystac.Link(
rel=pystac.RelType.ITEM,
target=item.self_href,
media_type=pystac.MediaType.GEOJSON,
title=f"Item {item.id}",
)
catalog.add_link(item_link)
return catalog
query_catalog = transform_item_collection_to_catalog_with_links(item_collection)Process Graph
from openeo.rest.stac_resource import StacResource
from openeo.internal.graph_building import PGNode
force_l2_stac_resource = StacResource(
graph=PGNode(
process_id="force_level2",
arguments={
"stac_document": query_catalog.to_dict(),
"name": processing_name,
"do_brdf": True,
}
),
connection=connection,
)
# Temporary fix
force_l2_stac_resource = StacResource(
graph=PGNode(
process_id="run_cwl_to_stac",
arguments=dict(
cwl="https://github.com/bcdev/apex-force-openeo/releases/latest/download/force-level2.cwl",
context={
"stac_document": query_catalog.to_dict(),
"name": processing_name,
"do_brdf": True,
}
),
),
connection=connection,
)
# Optional when only processing level 2
# If you intend to continue processing, or share the results with others,
# add the `export_workspace` call:
import getpass
# Be careful: The merge determines the storage location of your results.
# You may overwrite your results or those of others, if the `merge` parameter is not
# Set to a unique value.
merge = f"{getpass.getuser()}/experiments/{processing_name}"
print("merge: ", merge)
force_l2_stac_resource = force_l2_stac_resource.export_workspace(
workspace="apex-force-results-workspace",
merge=merge,
)merge: hannes/experiments/Modena
You can (optionally) inspect the process graph to visualize it.
{
"process_graph": {
"querystac1": {
"process_id": "query_stac",
"arguments": {
"spatial_extent": {
"west": 11.0,
"south": 44.5,
"east": 11.1,
"north": 44.6
},
"temporal_extent": [
"2026-04-17",
"2026-04-27"
],
"url": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-2-l1c"
}
},
"forcelevel21": {
"process_id": "force_level2",
"arguments": {
"aoi": "{ \"type\": \"Feature\", \"geometry\": { \"type\": \"Polygon\", \"coordinates\": [[[11.0,44.5],[11.0,44.6],[11.1,44.6],[11.1,44.5],[11.0,44.5]]] }, \"properties\": { \"name\": \"FORCE test\" } }",
"do_brdf": true,
"name": "Modena",
"stac_document": {
"from_node": "querystac1"
}
}
},
"exportworkspace1": {
"process_id": "export_workspace",
"arguments": {
"data": {
"from_node": "forcelevel21"
},
"merge": "hannes/experiments/Modena",
"workspace": "apex-force-results-workspace"
},
"result": true
}
}
}
{
"process_graph": {
"runcwltostac1": {
"process_id": "run_cwl_to_stac",
"arguments": {
"context": {
"stac_document": {
"type": "Catalog",
"id": "item-collection-catalog",
"stac_version": "1.1.0",
"description": "Catalog from item collection",
"links": [
{
"rel": "item",
"href": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-2-l1c/items/S2C_MSIL1C_20260427T101021_N0512_R022_T32TPQ_20260427T142022",
"type": "application/geo+json",
"title": "Item S2C_MSIL1C_20260427T101021_N0512_R022_T32TPQ_20260427T142022"
},
{
"rel": "item",
"href": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-2-l1c/items/S2B_MSIL1C_20260422T101019_N0512_R022_T32TPQ_20260422T134442",
"type": "application/geo+json",
"title": "Item S2B_MSIL1C_20260422T101019_N0512_R022_T32TPQ_20260422T134442"
},
{
"rel": "item",
"href": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-2-l1c/items/S2A_MSIL1C_20260419T100711_N0512_R022_T32TPQ_20260419T152521",
"type": "application/geo+json",
"title": "Item S2A_MSIL1C_20260419T100711_N0512_R022_T32TPQ_20260419T152521"
},
{
"rel": "item",
"href": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-2-l1c/items/S2C_MSIL1C_20260417T101021_N0512_R022_T32TPQ_20260417T134842",
"type": "application/geo+json",
"title": "Item S2C_MSIL1C_20260417T101021_N0512_R022_T32TPQ_20260417T134842"
}
]
},
"name": "Modena",
"do_brdf": true
},
"cwl": "https://github.com/bcdev/apex-force-openeo/releases/latest/download/force-level2.cwl"
}
},
"exportworkspace1": {
"process_id": "export_workspace",
"arguments": {
"data": {
"from_node": "runcwltostac1"
},
"merge": "hannes/experiments/Modena",
"workspace": "apex-force-results-workspace"
},
"result": true
}
}
}
Executing the Process Graph
With the process graph in place, we can submit it and wait for it to complete. You can monitor the progress through the client library or the web interface.
Accessing results
When the job has completed, we can inspect the generated STAC metadata of the FORCE data cube and download it.
Downloading is optional, if you wish to proceed with higher level processing. If your processing chain is complete after the level 2 module, you may download the results to continue your analysis.
The data cube is persistent on cloud storage, so you may download it at a later time or run multiple higher level processes with the same level 2 cube. The data cube is referenced by the URL to its STAC catalog (or item) when passing it to other processes.
While the cube will stay in place for multiple days, eventually the storage will be cleared. So please make sure to download your processing results for long term storage.
Inspecting STAC metadata
This step is optional
The basic URL of the FORCE workspace apex-force-results-workspace is https://s3.waw4-1.cloudferro.com/apex-force-results-waw4-1-exotc5yuexi2c5tvwqhoivj62fz8v0uupy0me. Together with the merge path defined for our processing, we can determine the path to the catalog.json for our FORCE level results:
from pystac.catalog import Catalog
WORKSPACE_URL = "https://s3.waw4-1.cloudferro.com/apex-force-results-waw4-1-exotc5yuexi2c5tvwqhoivj62fz8v0uupy0me"
l2_catalog_url = f"{WORKSPACE_URL}/{merge}/catalog.json"
l2_catalog = Catalog.from_file(l2_catalog_url)
l2_catalog.set_self_href(l2_catalog_url)
l2_catalogDownloading files
This step is optional if you plan to continue processing with FORCE on CDSE
Assets can be downloaded using the standard openEO mechanism to access result assets. The STAC metadata specifies the directory structure of the resulting cube. This structure is respected by openEO’s download_files.
To inspect a particular asset, we may use download_file to download singular files instead.
[PosixPath('force-level2-results/CITEME_0x6e.txt'),
PosixPath('force-level2-results/X0031_Y0029/20260417_LEVEL2_SEN2C_BOA.tif'),
PosixPath('force-level2-results/X0031_Y0029/20260417_LEVEL2_SEN2C_OVV.jpg'),
PosixPath('force-level2-results/X0031_Y0029/20260417_LEVEL2_SEN2C_QAI.tif'),
PosixPath('force-level2-results/X0031_Y0029/20260419_LEVEL2_SEN2A_BOA.tif'),
PosixPath('force-level2-results/X0031_Y0029/20260419_LEVEL2_SEN2A_OVV.jpg'),
PosixPath('force-level2-results/X0031_Y0029/20260419_LEVEL2_SEN2A_QAI.tif'),
PosixPath('force-level2-results/X0031_Y0029/20260422_LEVEL2_SEN2B_BOA.tif'),
PosixPath('force-level2-results/X0031_Y0029/20260422_LEVEL2_SEN2B_OVV.jpg'),
PosixPath('force-level2-results/X0031_Y0029/20260422_LEVEL2_SEN2B_QAI.tif'),
PosixPath('force-level2-results/X0031_Y0029/20260427_LEVEL2_SEN2C_BOA.tif'),
PosixPath('force-level2-results/X0031_Y0029/20260427_LEVEL2_SEN2C_OVV.jpg'),
PosixPath('force-level2-results/X0031_Y0029/20260427_LEVEL2_SEN2C_QAI.tif'),
PosixPath('force-level2-results/X0031_Y0030/20260417_LEVEL2_SEN2C_BOA.tif'),
PosixPath('force-level2-results/X0031_Y0030/20260417_LEVEL2_SEN2C_OVV.jpg'),
PosixPath('force-level2-results/X0031_Y0030/20260417_LEVEL2_SEN2C_QAI.tif'),
PosixPath('force-level2-results/X0031_Y0030/20260419_LEVEL2_SEN2A_BOA.tif'),
PosixPath('force-level2-results/X0031_Y0030/20260419_LEVEL2_SEN2A_OVV.jpg'),
PosixPath('force-level2-results/X0031_Y0030/20260419_LEVEL2_SEN2A_QAI.tif'),
PosixPath('force-level2-results/X0031_Y0030/20260427_LEVEL2_SEN2C_BOA.tif'),
PosixPath('force-level2-results/X0031_Y0030/20260427_LEVEL2_SEN2C_OVV.jpg'),
PosixPath('force-level2-results/X0031_Y0030/20260427_LEVEL2_SEN2C_QAI.tif'),
PosixPath('force-level2-results/X0032_Y0029/20260417_LEVEL2_SEN2C_BOA.tif'),
PosixPath('force-level2-results/X0032_Y0029/20260417_LEVEL2_SEN2C_OVV.jpg'),
PosixPath('force-level2-results/X0032_Y0029/20260417_LEVEL2_SEN2C_QAI.tif'),
PosixPath('force-level2-results/X0032_Y0029/20260419_LEVEL2_SEN2A_BOA.tif'),
PosixPath('force-level2-results/X0032_Y0029/20260419_LEVEL2_SEN2A_OVV.jpg'),
PosixPath('force-level2-results/X0032_Y0029/20260419_LEVEL2_SEN2A_QAI.tif'),
PosixPath('force-level2-results/X0032_Y0029/20260422_LEVEL2_SEN2B_BOA.tif'),
PosixPath('force-level2-results/X0032_Y0029/20260422_LEVEL2_SEN2B_OVV.jpg'),
PosixPath('force-level2-results/X0032_Y0029/20260422_LEVEL2_SEN2B_QAI.tif'),
PosixPath('force-level2-results/X0032_Y0029/20260427_LEVEL2_SEN2C_BOA.tif'),
PosixPath('force-level2-results/X0032_Y0029/20260427_LEVEL2_SEN2C_OVV.jpg'),
PosixPath('force-level2-results/X0032_Y0029/20260427_LEVEL2_SEN2C_QAI.tif'),
PosixPath('force-level2-results/X0032_Y0030/20260417_LEVEL2_SEN2C_BOA.tif'),
PosixPath('force-level2-results/X0032_Y0030/20260417_LEVEL2_SEN2C_OVV.jpg'),
PosixPath('force-level2-results/X0032_Y0030/20260417_LEVEL2_SEN2C_QAI.tif'),
PosixPath('force-level2-results/X0032_Y0030/20260419_LEVEL2_SEN2A_BOA.tif'),
PosixPath('force-level2-results/X0032_Y0030/20260419_LEVEL2_SEN2A_OVV.jpg'),
PosixPath('force-level2-results/X0032_Y0030/20260419_LEVEL2_SEN2A_QAI.tif'),
PosixPath('force-level2-results/X0032_Y0030/20260422_LEVEL2_SEN2B_BOA.tif'),
PosixPath('force-level2-results/X0032_Y0030/20260422_LEVEL2_SEN2B_OVV.jpg'),
PosixPath('force-level2-results/X0032_Y0030/20260422_LEVEL2_SEN2B_QAI.tif'),
PosixPath('force-level2-results/X0032_Y0030/20260427_LEVEL2_SEN2C_BOA.tif'),
PosixPath('force-level2-results/X0032_Y0030/20260427_LEVEL2_SEN2C_OVV.jpg'),
PosixPath('force-level2-results/X0032_Y0030/20260427_LEVEL2_SEN2C_QAI.tif'),
PosixPath('force-level2-results/datacube-definition.prj'),
PosixPath('force-level2-results/job-results.json')]