Computing Leaf Area Index Using Sentinel-2 Data

By: @dmeaux

Introduction

This notebook computes Leaf Area Index (LAI) 🌿 from Sentinel-2 Level-2A satellite data. Input data is read directly from cloud-native Zarr archives published by the Earth Observation Processing Framework (EOPF), so no SAFE archive downloads are required, saving what can take several minutes per Sentinel-2 L2A product. It processes each Sentinel-2 tile in full β€” consistent with the behaviour of the SNAP BiophysicalOp processor. Each STAC item returned by the query is processed independently and saved as a separate Zarr store under OUTPUT_DIR.

What we will learn

  • πŸ—‚οΈ The difference between using EOPF Zarr datasets and SAFE archives for processing Sentinel-2 data.
  • πŸ”Ž How to integrate EOPF STAC queries and results into a processing pipeline workflow using a neural network inferrence framework.
  • πŸ›°οΈ How to extract sensor metadata, such as sun and view zenith and azimuth angles, for computations.
  • πŸ“š How to combine extracted metadata with observation data for neural network inference.
  • βš™οΈ How to run stacked tiles through neural netowrk inference.
  • πŸ“˜ How to save results to the Zarr format.
  • πŸ—ΊοΈ How to visualize results using an interactive map.
  • βš™οΈβš™οΈ How to batch process a full growing season.

Prerequisites

⚠️ This is a more advanced walkthrough, so you should have experience processing data cubes using Python with Xarray and NumPy.


πŸ“œ Background

Traditional LAI processing with SNAP’s BiophysicalOp begins with downloading an entire Sentinel-2 L2A SAFE archive β€” typically 600 MB to 1.2 GB per granule β€” before a single pixel can be processed. On a 100 Mbps connection that equates to roughly 1–2 minutes of idle waiting per tile; on the slower connections common in research institutions or field deployments it can exceed 10 minutes. This notebook eliminates that bottleneck entirely. By opening EOPF products as lazy xarray DataTree objects backed by cloud-hosted Zarr stores, only the compressed chunks are fetched from object storage.

Scaling to a time series or multi-region study exposes an even larger gap. SNAP requires loading each product in the GUI, configuring the BiophysicalOp operator, and exporting results one at a time β€” a workflow that does not parallelise or automate easily. With the EOPF STAC API, expanding from a single acquisition to a full growing-season time series is a matter of widening the date window and wrapping the processing step in a loop. A European site typically receives 20–30 cloud-free Sentinel-2 passes between April and October; processing all of them in SNAP demands downloading 12–36 GB and chaining 20–30 manual processing runs β€” a multi-hour exercise even on fast hardware. The same campaign with this notebook streams only the required chunks per scene and processes each tile in approximately 60-90 seconds (with GPU acceleration for running the LAI neural network), compressing the entire time series into an unattended run measured in minutes rather than hours.

Beyond speed, the workflow is designed for reproducibility and frictionless deployment. SNAP carries a roughly 1 GB installation footprint, requires a Java runtime, and exposes its automation interface through the gpt command-line operator β€” a non-trivial barrier for containerised cloud workers, CI pipelines, or shared compute environments. This notebook’s entire software stack is declared in a single pixi.toml lockfile and installed with one command on any platform. Results are written to CF-compliant Zarr stores β€” a format that can be streamed directly by Python or a web mapping service without further conversion. Together, these properties make the workflow naturally suited to reproducible research and cloud-native execution at scale.

🌿 What is LAI?

Leaf Area Index (LAI) is a dimensionless biophysical quantity defined as half the total developed area of photosynthetically active elements of the vegetation per unit horizontal ground surface area (m² m⁻²). It is one of the Essential Climate Variables (ECVs) recognised by the Global Climate Observing System (GCOS) and underpins a wide range of Earth observation applications:

  • Agriculture β€” crop monitoring, yield forecasting, and irrigation scheduling
  • Ecology β€” forest carbon estimation, phenology studies, and habitat mapping
  • Climate science β€” land surface model parameterisation and validation
  • Environmental monitoring β€” deforestation detection and drought assessment

Typical LAI values range from 0 (bare soil) to roughly 8–10 mΒ² m⁻² for dense closed-canopy forests or intensively managed crops at peak growth.

πŸ›°οΈ Origins: The SNAP Sentinel-2 Optical Toolbox

The algorithm implemented here is a Python re-implementation of the BiophysicalOp processor from ESA’s open-source SNAP (Sentinel Application Platform), specifically the Optical Toolbox. The underlying science is described in the S2ToolBox Algorithm Theoretical Basis Document (ATBD):

Weiss, M., Baret, F., & Jay, S. (2020). S2ToolBox Level 2 products: LAI, FAPAR, FCOVER – Version 2.1.

The method employs a shallow two-layer feed-forward neural network trained over a large look-up table generated by the PROSAIL radiative-transfer model (PROSPECT for leaf optics + SAIL for canopy architecture). ESA distributes sensor-specific network weight files for Sentinel-2A, -2B, and -2C at both 10 m and 20 m spatial resolutions; this notebook automatically selects the correct weights from the detected satellite ID.

Differences from the Reference Java Implementation

Aspect SNAP (Java) This Notebook
Input data Local SAFE files EOPF Zarr via object storage
NN runtime Java native arrays Modular MAX engine (compiled graph)
Output format BEAM-DIMAP CF-compliant Zarr + Cloud-Optimised GeoTIFF

βš™οΈ Processing Pipeline

The workflow follows the original SNAP BiophysicalOp processing chain:

  1. STAC Discovery β€” query the EOPF catalog for scenes intersecting the AOI and date window
  2. Angle Interpolation β€” bilinearly interpolate the sparse 23 Γ— 23 angle grids to full image resolution, per detector module
  3. Input Assembly β€” stack 8 reflectance bands with 3 trigonometric angle terms (11 features total)
  4. Input Validation β€” flag pixels outside the NN training domain (per-band min/max check + convex-hull check)
  5. NN Inference β€” two-layer tanh network executed by the Modular MAX engine
  6. Output Validation β€” flag and clip outputs to physically plausible bounds
  7. Export β€” write LAI and quality flags to Zarr and/or GeoTIFF

The following modules cover each stage of the processing pipeline:

Module Role
pystac.client Query the EOPF STAC catalog to discover available Sentinel-2 L2A products by spatial extent, time range, and cloud cover
xarray Lazily open and navigate the hierarchical EOPF Zarr stores as DataTree objects β€” only the requested array chunks are fetched from object storage
numpy Efficient array operations for angle computation, input stacking, masking, and result post-processing
biophysical.angles Detector-footprint-aware interpolation and aggregation of per-band view zenith/azimuth angle grids
biophysical.io Save LAI values and quality flags to CF-compliant Zarr or Cloud-Optimised GeoTIFF
biophysical.models Enum types defining supported sensors (S2A, S2B, S2C at 10 m / 20 m) and biophysical variables (LAI)
biophysical.processing Full BiophysicalOp pipeline: input validation β†’ MAX-accelerated NN inference β†’ output validation
biophysical.utils Bilinear interpolation helpers and Sentinel-2 product name parser

Note: This notebook takes advantage of the biophysical package included with it due to the complexity of the above algorithm and its accompanying pre- and post-processing requirements.


πŸ› οΈ Setup

Environment Setup with uv

This notebook uses Modular’s MAX Inference Engine for neural network inference because of its ability to run the same code on either the CPU or any of several different GPUs from NVIDIA, AMD, and soon Apple, if one is available. The biophysicalop package provides the LAI retrieval implementation.

There are two ways to add it to your project:


Option B β€” pip install in a notebook cell

For environments where modular version 26.2 is already available, install the package directly from within a notebook cell:

%pip install git+https://github.com/Geomatys/biophysicalop.git

βš™οΈ Processing Pipeline

import sys
sys.path.insert(0, ".")  # Add current directory to sys.path for local imports

import shutil
from pathlib import Path

import warnings , os, s3fs

import branca.colormap as bcm
import folium
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
import numpy as np
import xarray as xr
import zarr
from branca.element import Element
from IPython.display import display
from pyproj import Transformer
from pystac.client import Client

from biophysicalop.angles import (
    add_detector_dimension,
    select_angle_by_detector_mask,
    compute_view_angle_mean,
    downsample_detector_masks,
    )
from biophysicalop.io import save_zarr, epsg_from_tile_id
from biophysicalop.models import BiophysicalVariables, SatelliteSensors
from biophysicalop.processing import BiophysicalOpProcess, process
from biophysicalop.utils import bilinear_interpolate_xy, parse_sentinel2_product_name

from save_zarr_utils import (
    prep_zarr
)

We prepare our credentials for S3 access:

warnings.filterwarnings("ignore")
try:
    bucket = os.environ["BUCKET_NAME"]
    access = os.environ["ACCESS_KEY"]
    secret = os.environ["SECRET_KEY"]
    bucket_endpoint = os.environ["BUCKET_ENDPOINT"]
except KeyError as e:
    raise RuntimeError(
        f"Missing required environment variable: {e}\n"
        "Please set BUCKET_NAME, ACCESS_KEY, and SECRET_KEY"
    )

# S3 filesystem
fs = s3fs.S3FileSystem(
    key=access,
    secret=secret,
    client_kwargs={"endpoint_url": bucket_endpoint},
)

Setup the constants to be used in the processing

The parameters below control what data is fetched and at what resolution. The most important choices are:

Time and quality

  • START_DATE_TIME / END_DATE_TIME β€” ISO-8601 UTC bounds for the temporal search.
  • CLOUD_COVER_MAX β€” Maximum acceptable cloud cover as a percentage (i.e. 5.0 = 5.0%)
  • OUTPUT_DIR β€” Directory where per-tile Zarr stores are written.

Spatial resolution

  • SPATIAL_RESOLUTION = 20 (recommended) β€” uses 8 spectral bands (B03–B12, excluding B08) and the 20 m NN weights. Produces a 5 490 Γ— 5 490 pixel grid for a standard 100 km Γ— 100 km Sentinel-2 tile and matches the reference SNAP BiophysicalOp output.
  • SPATIAL_RESOLUTION = 10 β€” uses only 3 bands (B03, B04, B08) with a separate, lighter 10 m network. Produces a 10 980 Γ— 10 980 grid at the cost of reduced spectral diversity.

Band and geometry lookups

  • SENTINEL2_BAND_GROUPS β€” maps each band name to its native resolution group inside the EOPF Zarr hierarchy (r10m, r20m, or r60m).
  • SENTINEL2_GEOMETRY_BAND_INDICES β€” maps spectral band names to the integer index used inside conditions/geometry.
  • SENTINEL2_LAI_BANDS_20M β€” the ordered list of 8 reflectance bands that form the first 8 features of the 20 m neural network input vector.
# User-defined parameters
# --------------------------------------------------------------------------------
# [minx, miny, maxx, maxy]
AOI_BBOX = (3.1360, 43.4477, 4.1134, 44.1579)  # an inset of tile 31TEJ, covering Montpellier, France

# Temporal range for Sentinel-2 data filtering (in ISO 8601 format)
# These dates are chosen to cover the growing season in the Northern Hemisphere,
# which is typically from April to October.
START_DATE_TIME = "2025-04-01T00:00:00Z"
END_DATE_TIME = "2025-10-31T23:59:59.999999Z"

CLOUD_COVER_MAX = 5.0  # maximum acceptable cloud cover as a percentage (i.e. 5.0 = 5.0%)

OUTPUT_DIR = Path("lai_tiles")  # per-tile zarr output directory
# --------------------------------------------------------------------------------


TEMPORAL_RANGE = f"{START_DATE_TIME}/{END_DATE_TIME}"
SPATIAL_RESOLUTION = 20  # in meters, must be one of {10, 20} for Sentinel-2
SENTINEL2_BAND_GROUPS = {
    "b01": "r60m",
    "b02": "r10m",
    "b03": "r10m",
    "b04": "r10m",
    "b05": "r20m",
    "b06": "r20m",
    "b07": "r20m",
    "b08": "r10m",
    "b8a": "r20m",
    "b09": "r60m",
    "b10": "r60m",
    "b11": "r20m",
    "b12": "r20m",
}
SENTINEL2_GEOMETRY_BAND_INDICES = {
    "b01": "b00",
    "b02": "b01",
    "b03": "b02",
    "b04": "b03",
    "b05": "b04",
    "b06": "b05",
    "b07": "b06",
    "b08": "b07",
    "b8a": "b08",
    "b09": "b09",
    "b10": "b10",
    "b11": "b11",
    "b12": "b12",
}
SENTINEL2_LAI_BANDS_10M = (
    "b03",
    "b04",
    "b08",
)
SENTINEL2_LAI_BANDS_20M = (
    "b03",
    "b04",
    "b05",
    "b06",
    "b07",
    "b8a",
    "b11",
    "b12",
    )

# Constants for EOPF
SAT_COLLECTION = "sentinel-2-l2a"
EOPF_STAC_API_ENDPOINT = "https://stac.core.eopf.eodc.eu/"

πŸ—‚οΈ The Difference Between Using EOPF Zarr Datasets and SAFE Archives

The Earth Observation Processing Framework (EOPF) repackages ESA Copernicus products as Zarr archives on object storage. Zarr is a cloud-native, chunk-based array format: instead of downloading a multi-gigabyte SAFE archive, only the specific array chunks needed are fetched over HTTP, enabling efficient sub-region and sub-band access. As stated in the background, traditional LAI processing with the SNAP Optical Toolbox BiophysicalOp processor begins with downloading an entire Sentinel-2 L2A SAFE archive, usually requiring 600 MB to 1.2 GB to be downloaded per granule.

πŸ”Ž Integrating EOPF STAC Queries and Results into a processing Pipeline

Search EOPF STAC for Sentinel-2 Images

Use the user defined time range from the above constants to query for Sentinel-2 Products.

Product discovery follows the open SpatioTemporal Asset Catalog (STAC) specification. Each STAC item represents one Sentinel-2 granule and includes a "product" asset pointing to the root URL of its Zarr store.

The search below returns Sentinel-2 L2A granules that: - Spatially intersect the AOI_BBOX bounding box - Temporally overlap the TEMPORAL_RANGE window - Have less than CLOUD_COVER_MAX% cloud cover (using the ESA-estimated eo:cloud_cover metadata field)

eopf_catalog = Client.open(url=EOPF_STAC_API_ENDPOINT)
s2_results = eopf_catalog.search(
    collections=[SAT_COLLECTION],
    bbox=AOI_BBOX,
    datetime=TEMPORAL_RANGE,
    query={"eo:cloud_cover": {"lt": CLOUD_COVER_MAX}}  # Filter for low cloud cover
)
items = tuple(s2_results.items())
print(f"Total Sentinel-2 L2A items found: {len(items)}")
for item in items:
    print(f"Item ID: {item.id}")
    print(f"  Geometry: {item.geometry}")
    print(f"  Bounding Box: {item.bbox}")
    print(f"  Datetime: {item.datetime}")
    print(f"  Cloud Cover: {item.properties.get('eo:cloud_cover', 'N/A')}")
    product = item.assets["product"]
    print(f"  Product HREF: {product.href}")  # Use product href to get zarr url
Total Sentinel-2 L2A items found: 15
Item ID: S2A_MSIL2A_20251030T104211_N0511_R008_T31TEJ_20251030T144716
  Geometry: {'type': 'Polygon', 'coordinates': [[[2.999749474120944, 44.25341758107761], [4.374940058749246, 44.24514241182411], [4.352494032483418, 43.256794294201605], [2.999753565511865, 43.26479037737325], [2.999749474120944, 44.25341758107761]]]}
  Bounding Box: [2.999749474120944, 43.256794294201605, 4.374940058749246, 44.25341758107761]
  Datetime: 2025-10-30 10:42:11.024000+00:00
  Cloud Cover: 0.713588
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2A_MSIL2A_20251030T104211_N0511_R008_T31TEJ_20251030T144716.zarr
Item ID: S2B_MSIL2A_20251030T103049_N0511_R108_T31TEJ_20251030T130433
  Geometry: {'type': 'Polygon', 'coordinates': [[[4.005694176135717, 43.25884423693857], [4.006666737240778, 43.2618584522954], [4.054214641392845, 43.408714965095214], [4.102001492637269, 43.55556092722918], [4.15009382059985, 43.70238356209406], [4.198619251512779, 43.84908934443938], [4.247012902182092, 43.995914013230525], [4.296322214027034, 44.14255029482407], [4.330147398004033, 44.24541195035435], [4.374940058749246, 44.24514241182411], [4.352494032483418, 43.256794294201605], [4.005694176135717, 43.25884423693857]]]}
  Bounding Box: [4.005694176135717, 43.256794294201605, 4.374940058749246, 44.24541195035435]
  Datetime: 2025-10-30 10:30:49.024000+00:00
  Cloud Cover: 0.082116
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2B_MSIL2A_20251030T103049_N0511_R108_T31TEJ_20251030T130433.zarr
Item ID: S2C_MSIL2A_20251028T104151_N0511_R008_T31TEJ_20251028T145122
  Geometry: {'type': 'Polygon', 'coordinates': [[[2.999749474120944, 44.25341758107761], [4.374940058749246, 44.24514241182411], [4.352494032483418, 43.256794294201605], [2.999753565511865, 43.26479037737325], [2.999749474120944, 44.25341758107761]]]}
  Bounding Box: [2.999749474120944, 43.256794294201605, 4.374940058749246, 44.25341758107761]
  Datetime: 2025-10-28 10:41:51.025000+00:00
  Cloud Cover: 1.649756
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2C_MSIL2A_20251028T104151_N0511_R008_T31TEJ_20251028T145122.zarr
Item ID: S2C_MSIL2A_20251018T104051_N0511_R008_T31TEJ_20251018T205314
  Geometry: {'type': 'Polygon', 'coordinates': [[[2.999749735219841, 44.190326696329606], [3.080339941079615, 44.1770058700864], [3.080170636302186, 44.17652324772636], [3.080263323696628, 44.176506816370726], [3.07996368494882, 44.175653051885135], [3.35386759094171, 44.126916725834604], [3.354053308066213, 44.12743560297753], [3.354333588729886, 44.12738583299684], [3.354514055378814, 44.12788843271424], [3.354581669773897, 44.12787644622808], [3.354590676309644, 44.12790162287181], [3.358243193930242, 44.127227341720285], [3.372804642699915, 44.12464592981835], [3.372768770285561, 44.12454581534072], [3.644154551226836, 44.074446038723785], [3.644301827954194, 44.074852523185726], [3.646290132854245, 44.07448576793084], [3.661629478014961, 44.071509882675], [3.661172835930347, 44.070251005517264], [3.931814146994537, 44.017803429006285], [3.931932284678499, 44.01812510406819], [3.931939135312465, 44.01812377567094], [3.932226810709366, 44.01890469249662], [3.948922655063657, 44.01548321726738], [3.948769020412453, 44.01506485326411], [4.237104469035296, 43.95606006746774], [4.237037982837612, 43.95588185462314], [4.238085647405527, 43.955659811266806], [4.237886297413936, 43.95512701199173], [4.238034088381697, 43.9550957154162], [4.237925609151044, 43.95480486726842], [4.367723347430212, 43.92737467141782], [4.352494032483418, 43.256794294201605], [2.999753565511865, 43.26479037737325], [2.999749735219841, 44.190326696329606]]]}
  Bounding Box: [2.999749735219841, 43.256794294201605, 4.367723347430212, 44.190326696329606]
  Datetime: 2025-10-18 10:40:51.025000+00:00
  Cloud Cover: 0.409785
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2C_MSIL2A_20251018T104051_N0511_R008_T31TEJ_20251018T205314.zarr
Item ID: S2C_MSIL2A_20251018T104051_N0511_R008_T31TEJ_20251018T200113
  Geometry: {'type': 'Polygon', 'coordinates': [[[4.363694039141706, 43.74995532711596], [4.157368162595406, 43.79346341981967], [4.157638319129653, 43.7941901876288], [4.157591188638929, 43.79419986379704], [4.157611894201473, 43.79425557226157], [4.157562147972216, 43.7942657913887], [4.157741992589658, 43.79474893476958], [3.888329331098034, 43.85016310759728], [3.888269674712022, 43.85000068524352], [3.888227552877011, 43.850009342874394], [3.887905751285096, 43.8491322269987], [3.583984502544144, 43.90807962528523], [3.584264495792644, 43.90885361671259], [3.584252941555609, 43.90885575324023], [3.584476459424989, 43.90947277964113], [3.313848366818832, 43.95946280377139], [3.313799872198968, 43.95932740516691], [3.313762211068698, 43.95933436227994], [3.313470629605916, 43.95851939638556], [3.006287053779157, 44.013333526199695], [3.006588778942535, 44.01419031849532], [2.999750459510941, 44.015311915785524], [2.999749474120944, 44.25341758107761], [4.374940058749246, 44.24514241182411], [4.363694039141706, 43.74995532711596]]]}
  Bounding Box: [2.999749474120944, 43.74995532711596, 4.374940058749246, 44.25341758107761]
  Datetime: 2025-10-18 10:40:51.025000+00:00
  Cloud Cover: 0.65563
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2C_MSIL2A_20251018T104051_N0511_R008_T31TEJ_20251018T200113.zarr
Item ID: S2A_MSIL2A_20251017T103041_N0511_R108_T31TEJ_20251017T155021
  Geometry: {'type': 'Polygon', 'coordinates': [[[4.009598665597077, 43.258821157402444], [4.015407696185386, 43.2767208010392], [4.06316280730122, 43.42358105442678], [4.110894246531354, 43.57038983482697], [4.158693425647685, 43.71716151753356], [4.206996096092864, 43.863816709106246], [4.25510711981884, 44.01056802547626], [4.304059103877955, 44.15719324523995], [4.33312976289036, 44.24539400406059], [4.374940058749246, 44.24514241182411], [4.352494032483418, 43.256794294201605], [4.009598665597077, 43.258821157402444]]]}
  Bounding Box: [4.009598665597077, 43.256794294201605, 4.374940058749246, 44.24539400406059]
  Datetime: 2025-10-17 10:30:41.024000+00:00
  Cloud Cover: 0.079225
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2A_MSIL2A_20251017T103041_N0511_R108_T31TEJ_20251017T155021.zarr
Item ID: S2C_MSIL2A_20251015T103031_N0511_R108_T31TEJ_20251015T161117
  Geometry: {'type': 'Polygon', 'coordinates': [[[4.009840633040255, 43.25881972712676], [4.048973646286205, 43.38025370013829], [4.096295595835079, 43.527178549418565], [4.14381103027854, 43.674062901396816], [4.170348465596649, 43.75558371079492], [4.192183484413271, 43.82252533122775], [4.240110737137591, 43.969365249374015], [4.260182216799283, 44.02997724998347], [4.288269822020025, 44.1144251058632], [4.3313063232805, 44.245404976555285], [4.374940058749246, 44.24514241182411], [4.352494032483418, 43.256794294201605], [4.009840633040255, 43.25881972712676]]]}
  Bounding Box: [4.009840633040255, 43.256794294201605, 4.374940058749246, 44.245404976555285]
  Datetime: 2025-10-15 10:30:31.025000+00:00
  Cloud Cover: 0.027531
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2C_MSIL2A_20251015T103031_N0511_R108_T31TEJ_20251015T161117.zarr
Item ID: S2A_MSIL2A_20251010T104001_N0511_R008_T31TEJ_20251010T172518
  Geometry: {'type': 'Polygon', 'coordinates': [[[2.999751647531111, 43.72824351260538], [3.196169918404005, 43.693363313713085], [3.196219759900159, 43.69350318669725], [3.199216067303486, 43.6929703361596], [3.199403482706296, 43.69349686389792], [3.199622798471584, 43.69345793364484], [3.199877147377955, 43.694171491838055], [3.487026245631773, 43.64098954616449], [3.487166955732334, 43.64137999270429], [3.490161997825271, 43.6408255847484], [3.501472739139136, 43.63862606856076], [3.501065580989618, 43.637496909841886], [3.501109162888151, 43.63748843936892], [3.500891007117723, 43.63688283808694], [3.770586220690224, 43.584471752404816], [3.770744522107497, 43.58490579503936], [3.773369769440987, 43.58439513744098], [3.773831933369448, 43.5856623909434], [3.776305013657012, 43.585181469315465], [3.789823927300506, 43.58240661765728], [3.789700888334701, 43.58206987103524], [4.074260310509901, 43.52381547570125], [4.074225034692251, 43.52372015894707], [4.074455899276074, 43.52367105089948], [4.074437013109758, 43.523620117506105], [4.074518786667455, 43.523602737473446], [4.074005415212866, 43.52221593164724], [4.344670655368001, 43.46506504133068], [4.344710603564738, 43.46517165126475], [4.344959885842306, 43.46511905967128], [4.344990861999915, 43.46520182125857], [4.3473700608977, 43.464699065346885], [4.347705994578857, 43.46559640436082], [4.357187636230289, 43.46346403490077], [4.352494032483418, 43.256794294201605], [2.999753565511865, 43.26479037737325], [2.999751647531111, 43.72824351260538]]]}
  Bounding Box: [2.999751647531111, 43.256794294201605, 4.357187636230289, 43.72824351260538]
  Datetime: 2025-10-10 10:40:01.024000+00:00
  Cloud Cover: 0.00881
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2A_MSIL2A_20251010T104001_N0511_R008_T31TEJ_20251010T172518.zarr
Item ID: S2C_MSIL2A_20251005T102921_N0511_R108_T31TEJ_20251005T155605
  Geometry: {'type': 'Polygon', 'coordinates': [[[3.996902592802162, 43.25889620421039], [4.002550239623694, 43.2763128635939], [4.050358167958506, 43.42322155121074], [4.098264130761962, 43.57007655526528], [4.146294506040694, 43.71688208047622], [4.194763315665021, 43.86358218775699], [4.243058650581385, 44.010342845215725], [4.292051709437927, 44.15699201427013], [4.321205161369018, 44.24546576000262], [4.374940058749246, 44.24514241182411], [4.352494032483418, 43.256794294201605], [3.996902592802162, 43.25889620421039]]]}
  Bounding Box: [3.996902592802162, 43.256794294201605, 4.374940058749246, 44.24546576000262]
  Datetime: 2025-10-05 10:29:21.025000+00:00
  Cloud Cover: 0.620101
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2C_MSIL2A_20251005T102921_N0511_R108_T31TEJ_20251005T155605.zarr
Item ID: S2A_MSIL2A_20250907T103031_N0511_R108_T31TEJ_20250907T142815
  Geometry: {'type': 'Polygon', 'coordinates': [[[4.007933578555628, 43.25883099977413], [4.053914061479624, 43.400130355868015], [4.10200129449421, 43.5470258320004], [4.150286246823151, 43.693872715999696], [4.198977069700992, 43.84056658443097], [4.246877625970667, 43.98740530584433], [4.295831549412578, 44.13392803962985], [4.332295072100586, 44.245399026788085], [4.374940058749246, 44.24514241182411], [4.352494032483418, 43.256794294201605], [4.007933578555628, 43.25883099977413]]]}
  Bounding Box: [4.007933578555628, 43.256794294201605, 4.374940058749246, 44.245399026788085]
  Datetime: 2025-09-07 10:30:31.024000+00:00
  Cloud Cover: 4.397467
  Product HREF: https://data.eodc.eu/collections/EOPF_ZARR/products/cpm_v270/S02MSIL2A/2025/09/07/S2A_MSIL2A_20250907T103031_N0511_R108_T31TEJ_20250907T142815.zarr
Item ID: S2A_MSIL2A_20250530T103041_N0511_R108_T31TEJ_20250530T142711
  Geometry: {'type': 'Polygon', 'coordinates': [[[3.996151030886651, 43.258900646711915], [4.023793990467271, 43.34402002455392], [4.071660137619537, 43.490925621177965], [4.11966554524329, 43.63778121797476], [4.168007854238619, 43.7844957106434], [4.216194567973007, 43.93121158394251], [4.26500308831756, 44.07775724585984], [4.313670484861823, 44.22443496225287], [4.320641676579815, 44.24546915075591], [4.374940058749246, 44.24514241182411], [4.352494032483418, 43.256794294201605], [3.996151030886651, 43.258900646711915]]]}
  Bounding Box: [3.996151030886651, 43.256794294201605, 4.374940058749246, 44.24546915075591]
  Datetime: 2025-05-30 10:30:41.024000+00:00
  Cloud Cover: 0.023985
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2A_MSIL2A_20250530T103041_N0511_R108_T31TEJ_20250530T142711.zarr
Item ID: S2A_MSIL2A_20250430T102701_N0511_R108_T31TEJ_20250430T190517
  Geometry: {'type': 'Polygon', 'coordinates': [[[3.999719577844396, 43.258879552940435], [4.01066893050314, 43.292618330840085], [4.058667839661785, 43.43963555941233], [4.106914943696001, 43.586620584359615], [4.15531245648374, 43.73349867183368], [4.203877089687448, 43.88029686606191], [4.25234288801703, 44.02709602620672], [4.301478140176486, 44.17367392313218], [4.325129722814647, 44.24544214406853], [4.374940058749246, 44.24514241182411], [4.352494032483418, 43.256794294201605], [3.999719577844396, 43.258879552940435]]]}
  Bounding Box: [3.999719577844396, 43.256794294201605, 4.374940058749246, 44.24544214406853]
  Datetime: 2025-04-30 10:27:01.024000+00:00
  Cloud Cover: 0.047449
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2A_MSIL2A_20250430T102701_N0511_R108_T31TEJ_20250430T190517.zarr
Item ID: S2A_MSIL2A_20250420T103041_N0511_R108_T31TEJ_20250420T202912
  Geometry: {'type': 'Polygon', 'coordinates': [[[3.999054267090983, 43.258883485609154], [4.046727311476494, 43.40550124694311], [4.094877054390124, 43.55241547881358], [4.143268956901982, 43.69933392159373], [4.192035284878529, 43.846133169476616], [4.216811041370139, 43.92123219977511], [4.240519415716781, 43.99294472589098], [4.249841276796993, 44.02070565347856], [4.289983445930977, 44.139607974567234], [4.324815706219906, 44.24544403365424], [4.374940058749246, 44.24514241182411], [4.352494032483418, 43.256794294201605], [3.999054267090983, 43.258883485609154]]]}
  Bounding Box: [3.999054267090983, 43.256794294201605, 4.374940058749246, 44.24544403365424]
  Datetime: 2025-04-20 10:30:41.024000+00:00
  Cloud Cover: 1.786733
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2A_MSIL2A_20250420T103041_N0511_R108_T31TEJ_20250420T202912.zarr
Item ID: S2C_MSIL2A_20250418T103041_N0511_R108_T31TEJ_20250418T160655
  Geometry: {'type': 'Polygon', 'coordinates': [[[3.996524135750957, 43.258898441279605], [4.029442036161715, 43.36057738888665], [4.077147593719051, 43.50739796354541], [4.125125045129605, 43.6542448039122], [4.173779370552519, 43.80100107173931], [4.221976447715368, 43.94791608964147], [4.270987202437039, 44.09461300238891], [4.31946199620316, 44.24146793252264], [4.320796341402683, 44.24546822006485], [4.374940058749246, 44.24514241182411], [4.352494032483418, 43.256794294201605], [3.996524135750957, 43.258898441279605]]]}
  Bounding Box: [3.996524135750957, 43.256794294201605, 4.374940058749246, 44.24546822006485]
  Datetime: 2025-04-18 10:30:41.025000+00:00
  Cloud Cover: 0.165078
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2C_MSIL2A_20250418T103041_N0511_R108_T31TEJ_20250418T160655.zarr
Item ID: S2B_MSIL2A_20250406T103629_N0511_R008_T31TEJ_20250406T133008
  Geometry: {'type': 'Polygon', 'coordinates': [[[2.999749474120944, 44.25341758107761], [4.374940058749246, 44.24514241182411], [4.352494032483418, 43.256794294201605], [2.999753565511865, 43.26479037737325], [2.999749474120944, 44.25341758107761]]]}
  Bounding Box: [2.999749474120944, 43.256794294201605, 4.374940058749246, 44.25341758107761]
  Datetime: 2025-04-06 10:36:29.024000+00:00
  Cloud Cover: 0.004834
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2B_MSIL2A_20250406T103629_N0511_R008_T31TEJ_20250406T133008.zarr

🧭 Walkthrough

The cells below walk through each processing step in detail for the first scene returned by the STAC query. This single-scene walkthrough illustrates the full pipeline β€” angle interpolation, input assembly, NN inference, and output saving β€” before the batch processing section repeats the same logic efficiently for every scene.

# get the url to the product of the first item in the search results
product = items[0].assets["product"]
print(f"  Product HREF: {product.href}")

# lazily open the product zarr with xarray
dt = xr.open_datatree(product.href, engine="zarr")
dt
  Product HREF: https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:notebook-data/tutorial_data/cpm_v270/S2A_MSIL2A_20251030T104211_N0511_R008_T31TEJ_20251030T144716.zarr
<xarray.DataTree>
Group: /
β”‚   Attributes:
β”‚       other_metadata:      {'aot_retrieval_model': 'SEN2COR_DDV', 'band_descrip...
β”‚       processing_history:  {'Level-0 Product': [{'facility': 'ESA', 'inputs': '...
β”‚       stac_discovery:      {'assets': {'aod1240': {'href': '/conditions/meteoro...
β”œβ”€β”€ Group: /conditions
β”‚   β”œβ”€β”€ Group: /conditions/geometry
β”‚   β”‚       Dimensions:                        (angle: 2, band: 13, y: 23, x: 23,
β”‚   β”‚                                           detector: 7)
β”‚   β”‚       Coordinates:
β”‚   β”‚         * angle                          (angle) <U7 56B 'zenith' 'azimuth'
β”‚   β”‚         * band                           (band) <U3 156B 'b01' 'b02' ... 'b11' 'b12'
β”‚   β”‚         * y                              (y) float32 92B 4.9e+06 ... 4.79e+06
β”‚   β”‚         * x                              (x) float32 92B 5e+05 5.05e+05 ... 6.1e+05
β”‚   β”‚         * detector                       (detector) object 56B 'd04' 'd05' ... 'd10'
β”‚   β”‚       Data variables:
β”‚   β”‚           mean_sun_angles                (angle) float64 16B ...
β”‚   β”‚           mean_viewing_incidence_angles  (band, angle) float64 208B ...
β”‚   β”‚           sun_angles                     (angle, y, x) float64 8kB ...
β”‚   β”‚           viewing_incidence_angles       (band, detector, angle, y, x) float64 770kB ...
β”‚   β”œβ”€β”€ Group: /conditions/mask
β”‚   β”‚   β”œβ”€β”€ Group: /conditions/mask/detector_footprint
β”‚   β”‚   β”‚   β”œβ”€β”€ Group: /conditions/mask/detector_footprint/r10m
β”‚   β”‚   β”‚   β”‚       Dimensions:  (y: 10980, x: 10980)
β”‚   β”‚   β”‚   β”‚       Coordinates:
β”‚   β”‚   β”‚   β”‚         * y        (y) float32 44kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
β”‚   β”‚   β”‚   β”‚         * x        (x) float32 44kB 5e+05 5e+05 5e+05 ... 6.098e+05 6.098e+05
β”‚   β”‚   β”‚   β”‚       Data variables:
β”‚   β”‚   β”‚   β”‚           b02      (y, x) uint8 121MB ...
β”‚   β”‚   β”‚   β”‚           b03      (y, x) uint8 121MB ...
β”‚   β”‚   β”‚   β”‚           b04      (y, x) uint8 121MB ...
β”‚   β”‚   β”‚   β”‚           b08      (y, x) uint8 121MB ...
β”‚   β”‚   β”‚   β”œβ”€β”€ Group: /conditions/mask/detector_footprint/r20m
β”‚   β”‚   β”‚   β”‚       Dimensions:  (y: 5490, x: 5490)
β”‚   β”‚   β”‚   β”‚       Coordinates:
β”‚   β”‚   β”‚   β”‚         * y        (y) float32 22kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
β”‚   β”‚   β”‚   β”‚         * x        (x) float32 22kB 5e+05 5e+05 5e+05 ... 6.098e+05 6.098e+05
β”‚   β”‚   β”‚   β”‚       Data variables:
β”‚   β”‚   β”‚   β”‚           b05      (y, x) uint8 30MB ...
β”‚   β”‚   β”‚   β”‚           b06      (y, x) uint8 30MB ...
β”‚   β”‚   β”‚   β”‚           b07      (y, x) uint8 30MB ...
β”‚   β”‚   β”‚   β”‚           b11      (y, x) uint8 30MB ...
β”‚   β”‚   β”‚   β”‚           b12      (y, x) uint8 30MB ...
β”‚   β”‚   β”‚   β”‚           b8a      (y, x) uint8 30MB ...
β”‚   β”‚   β”‚   └── Group: /conditions/mask/detector_footprint/r60m
β”‚   β”‚   β”‚           Dimensions:  (y: 1830, x: 1830)
β”‚   β”‚   β”‚           Coordinates:
β”‚   β”‚   β”‚             * y        (y) float32 7kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
β”‚   β”‚   β”‚             * x        (x) float32 7kB 5e+05 5.001e+05 5.001e+05 ... 6.097e+05 6.098e+05
β”‚   β”‚   β”‚           Data variables:
β”‚   β”‚   β”‚               b01      (y, x) uint8 3MB ...
β”‚   β”‚   β”‚               b09      (y, x) uint8 3MB ...
β”‚   β”‚   β”‚               b10      (y, x) uint8 3MB ...
β”‚   β”‚   β”œβ”€β”€ Group: /conditions/mask/l1c_classification
β”‚   β”‚   β”‚   └── Group: /conditions/mask/l1c_classification/r60m
β”‚   β”‚   β”‚           Dimensions:  (y: 1830, x: 1830)
β”‚   β”‚   β”‚           Coordinates:
β”‚   β”‚   β”‚             * y        (y) float32 7kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
β”‚   β”‚   β”‚             * x        (x) float32 7kB 5e+05 5.001e+05 5.001e+05 ... 6.097e+05 6.098e+05
β”‚   β”‚   β”‚           Data variables:
β”‚   β”‚   β”‚               b00      (y, x) uint8 3MB ...
β”‚   β”‚   └── Group: /conditions/mask/l2a_classification
β”‚   β”‚       β”œβ”€β”€ Group: /conditions/mask/l2a_classification/r20m
β”‚   β”‚       β”‚       Dimensions:  (y: 5490, x: 5490)
β”‚   β”‚       β”‚       Coordinates:
β”‚   β”‚       β”‚         * y        (y) float32 22kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
β”‚   β”‚       β”‚         * x        (x) float32 22kB 5e+05 5e+05 5e+05 ... 6.098e+05 6.098e+05
β”‚   β”‚       β”‚       Data variables:
β”‚   β”‚       β”‚           scl      (y, x) float64 241MB ...
β”‚   β”‚       └── Group: /conditions/mask/l2a_classification/r60m
β”‚   β”‚               Dimensions:  (y: 1830, x: 1830)
β”‚   β”‚               Coordinates:
β”‚   β”‚                 * y        (y) float32 7kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
β”‚   β”‚                 * x        (x) float32 7kB 5e+05 5.001e+05 5.001e+05 ... 6.097e+05 6.098e+05
β”‚   β”‚               Data variables:
β”‚   β”‚                   scl      (y, x) float64 27MB ...
β”‚   └── Group: /conditions/meteorology
β”‚       β”œβ”€β”€ Group: /conditions/meteorology/cams
β”‚       β”‚       Dimensions:    (latitude: 9, longitude: 9)
β”‚       β”‚       Coordinates:
β”‚       β”‚         * latitude   (latitude) float32 36B 44.25 44.13 44.0 ... 43.5 43.38 43.26
β”‚       β”‚         * longitude  (longitude) float32 36B 3.0 3.169 3.338 ... 4.015 4.184 4.353
β”‚       β”‚       Data variables:
β”‚       β”‚           aod1240    (latitude, longitude) float32 324B ...
β”‚       β”‚           aod469     (latitude, longitude) float32 324B ...
β”‚       β”‚           aod550     (latitude, longitude) float32 324B ...
β”‚       β”‚           aod670     (latitude, longitude) float32 324B ...
β”‚       β”‚           aod865     (latitude, longitude) float32 324B ...
β”‚       β”‚           bcaod550   (latitude, longitude) float32 324B ...
β”‚       β”‚           duaod550   (latitude, longitude) float32 324B ...
β”‚       β”‚           omaod550   (latitude, longitude) float32 324B ...
β”‚       β”‚           ssaod550   (latitude, longitude) float32 324B ...
β”‚       β”‚           suaod550   (latitude, longitude) float32 324B ...
β”‚       β”‚           z          (latitude, longitude) float32 324B ...
β”‚       └── Group: /conditions/meteorology/ecmwf
β”‚               Dimensions:    (latitude: 9, longitude: 9)
β”‚               Coordinates:
β”‚                 * latitude   (latitude) float32 36B 44.25 44.13 44.0 ... 43.5 43.38 43.26
β”‚                 * longitude  (longitude) float32 36B 3.0 3.169 3.338 ... 4.015 4.184 4.353
β”‚               Data variables:
β”‚                   msl        (latitude, longitude) float32 324B ...
β”‚                   r          (latitude, longitude) float32 324B ...
β”‚                   tco3       (latitude, longitude) float32 324B ...
β”‚                   tcwv       (latitude, longitude) float32 324B ...
β”‚                   u10        (latitude, longitude) float32 324B ...
β”‚                   v10        (latitude, longitude) float32 324B ...
β”œβ”€β”€ Group: /measurements
β”‚   └── Group: /measurements/reflectance
β”‚       β”œβ”€β”€ Group: /measurements/reflectance/r10m
β”‚       β”‚       Dimensions:  (y: 10980, x: 10980)
β”‚       β”‚       Coordinates:
β”‚       β”‚         * y        (y) float32 44kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
β”‚       β”‚         * x        (x) float32 44kB 5e+05 5e+05 5e+05 ... 6.098e+05 6.098e+05
β”‚       β”‚       Data variables:
β”‚       β”‚           b02      (y, x) float64 964MB ...
β”‚       β”‚           b03      (y, x) float64 964MB ...
β”‚       β”‚           b04      (y, x) float64 964MB ...
β”‚       β”‚           b08      (y, x) float64 964MB ...
β”‚       β”œβ”€β”€ Group: /measurements/reflectance/r20m
β”‚       β”‚       Dimensions:  (y: 5490, x: 5490)
β”‚       β”‚       Coordinates:
β”‚       β”‚         * y        (y) float32 22kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
β”‚       β”‚         * x        (x) float32 22kB 5e+05 5e+05 5e+05 ... 6.098e+05 6.098e+05
β”‚       β”‚       Data variables:
β”‚       β”‚           b01      (y, x) float64 241MB ...
β”‚       β”‚           b02      (y, x) float64 241MB ...
β”‚       β”‚           b03      (y, x) float64 241MB ...
β”‚       β”‚           b04      (y, x) float64 241MB ...
β”‚       β”‚           b05      (y, x) float64 241MB ...
β”‚       β”‚           b06      (y, x) float64 241MB ...
β”‚       β”‚           b07      (y, x) float64 241MB ...
β”‚       β”‚           b11      (y, x) float64 241MB ...
β”‚       β”‚           b12      (y, x) float64 241MB ...
β”‚       β”‚           b8a      (y, x) float64 241MB ...
β”‚       └── Group: /measurements/reflectance/r60m
β”‚               Dimensions:  (y: 1830, x: 1830)
β”‚               Coordinates:
β”‚                 * y        (y) float32 7kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
β”‚                 * x        (x) float32 7kB 5e+05 5.001e+05 5.001e+05 ... 6.097e+05 6.098e+05
β”‚               Data variables:
β”‚                   b01      (y, x) float64 27MB ...
β”‚                   b02      (y, x) float64 27MB ...
β”‚                   b03      (y, x) float64 27MB ...
β”‚                   b04      (y, x) float64 27MB ...
β”‚                   b05      (y, x) float64 27MB ...
β”‚                   b06      (y, x) float64 27MB ...
β”‚                   b07      (y, x) float64 27MB ...
β”‚                   b09      (y, x) float64 27MB ...
β”‚                   b11      (y, x) float64 27MB ...
β”‚                   b12      (y, x) float64 27MB ...
β”‚                   b8a      (y, x) float64 27MB ...
└── Group: /quality
    β”œβ”€β”€ Group: /quality/atmosphere
    β”‚   β”œβ”€β”€ Group: /quality/atmosphere/r10m
    β”‚   β”‚       Dimensions:  (y: 10980, x: 10980)
    β”‚   β”‚       Coordinates:
    β”‚   β”‚         * y        (y) float32 44kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
    β”‚   β”‚         * x        (x) float32 44kB 5e+05 5e+05 5e+05 ... 6.098e+05 6.098e+05
    β”‚   β”‚       Data variables:
    β”‚   β”‚           aot      (y, x) float64 964MB ...
    β”‚   β”‚           wvp      (y, x) float64 964MB ...
    β”‚   β”œβ”€β”€ Group: /quality/atmosphere/r20m
    β”‚   β”‚       Dimensions:  (y: 5490, x: 5490)
    β”‚   β”‚       Coordinates:
    β”‚   β”‚         * y        (y) float32 22kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
    β”‚   β”‚         * x        (x) float32 22kB 5e+05 5e+05 5e+05 ... 6.098e+05 6.098e+05
    β”‚   β”‚       Data variables:
    β”‚   β”‚           aot      (y, x) float64 241MB ...
    β”‚   β”‚           wvp      (y, x) float64 241MB ...
    β”‚   └── Group: /quality/atmosphere/r60m
    β”‚           Dimensions:  (y: 1830, x: 1830)
    β”‚           Coordinates:
    β”‚             * y        (y) float32 7kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
    β”‚             * x        (x) float32 7kB 5e+05 5.001e+05 5.001e+05 ... 6.097e+05 6.098e+05
    β”‚           Data variables:
    β”‚               aot      (y, x) float64 27MB ...
    β”‚               wvp      (y, x) float64 27MB ...
    β”œβ”€β”€ Group: /quality/mask
    β”‚   β”œβ”€β”€ Group: /quality/mask/r10m
    β”‚   β”‚       Dimensions:  (y: 10980, x: 10980)
    β”‚   β”‚       Coordinates:
    β”‚   β”‚         * y        (y) float32 44kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
    β”‚   β”‚         * x        (x) float32 44kB 5e+05 5e+05 5e+05 ... 6.098e+05 6.098e+05
    β”‚   β”‚       Data variables:
    β”‚   β”‚           b02      (y, x) uint8 121MB ...
    β”‚   β”‚           b03      (y, x) uint8 121MB ...
    β”‚   β”‚           b04      (y, x) uint8 121MB ...
    β”‚   β”‚           b08      (y, x) uint8 121MB ...
    β”‚   β”œβ”€β”€ Group: /quality/mask/r20m
    β”‚   β”‚       Dimensions:  (y: 5490, x: 5490)
    β”‚   β”‚       Coordinates:
    β”‚   β”‚         * y        (y) float32 22kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
    β”‚   β”‚         * x        (x) float32 22kB 5e+05 5e+05 5e+05 ... 6.098e+05 6.098e+05
    β”‚   β”‚       Data variables:
    β”‚   β”‚           b05      (y, x) uint8 30MB ...
    β”‚   β”‚           b06      (y, x) uint8 30MB ...
    β”‚   β”‚           b07      (y, x) uint8 30MB ...
    β”‚   β”‚           b11      (y, x) uint8 30MB ...
    β”‚   β”‚           b12      (y, x) uint8 30MB ...
    β”‚   β”‚           b8a      (y, x) uint8 30MB ...
    β”‚   └── Group: /quality/mask/r60m
    β”‚           Dimensions:  (y: 1830, x: 1830)
    β”‚           Coordinates:
    β”‚             * y        (y) float32 7kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
    β”‚             * x        (x) float32 7kB 5e+05 5.001e+05 5.001e+05 ... 6.097e+05 6.098e+05
    β”‚           Data variables:
    β”‚               b01      (y, x) uint8 3MB ...
    β”‚               b09      (y, x) uint8 3MB ...
    β”‚               b10      (y, x) uint8 3MB ...
    └── Group: /quality/probability
        └── Group: /quality/probability/r20m
                Dimensions:  (y: 5490, x: 5490)
                Coordinates:
                  * y        (y) float32 22kB 4.9e+06 4.9e+06 4.9e+06 ... 4.79e+06 4.79e+06
                  * x        (x) float32 22kB 5e+05 5e+05 5e+05 ... 6.098e+05 6.098e+05
                Data variables:
                    cld      (y, x) float32 121MB ...
                    snw      (y, x) float32 121MB ...

The EOPF Zarr store is opened lazily as an xarray.DataTree β€” a hierarchical tree of Dataset nodes that mirrors the internal Zarr group structure. No pixel data is downloaded at this point; arrays are only fetched when explicitly accessed or computed.

The main sub-trees used in this notebook are:

Group path Contents
measurements/reflectance/r10m Bottom-of-Atmosphere (BOA) reflectance at 10 m β€” bands B02, B03, B04, B08
measurements/reflectance/r20m BOA reflectance at 20 m β€” bands B05, B06, B07, B8A, B11, B12
conditions/geometry Sun and per-band, per-detector view angle grids (23 Γ— 23 points at ~5 km spacing)
conditions/mask/detector_footprint/r10m and r20m Per-detector binary footprint masks indicating which detector module observed each pixel
quality/l2a_quicklook/r60m True-Colour Image (TCI) at 60 m for quick visual inspection

Reflectance scaling β€” bands are stored as scaled uint16 integers. The physical BOA reflectance is recovered as: reflectance = (raw_uint16 βˆ’ 1000) / 10 000 (BOA_ADD_OFFSET = βˆ’1000, QUANTIFICATION_VALUE = 10 000, per the Sentinel-2 Product Specification.)

Build a True-Color Composite

The true-colour composite (B04 red, B03 green, B02 blue) at 60 m resolution serves as a quick visual sanity check to confirm:

  • The correct granule was selected (scene footprint covers the AOI)
  • Cloud cover is within acceptable limits for the LAI retrieval
  • The image is correctly georeferenced when overlaid on the CartoDB basemap

The composite is built directly from the measurements/reflectance/r60m group and displayed with a 2–98 % percentile linear stretch for natural-looking contrast. The LAI product is computed at 20 m.

Note: If you see the message Make this Notebook Trusted to load map: File -> Trust Notebook instead of a map after running the next cell, run the code in folium_test.ipynb and then click back on this notebook’s tab. The map should then be displayed.

# ---- Build a true-colour RGB composite from 60 m reflectance bands ----
# R = B04 (red), G = B03 (green), B = B02 (blue)
r60m = dt["measurements/reflectance/r60m"]
band_r = r60m["b04"].values  # red   – BOA reflectance, float32
band_g = r60m["b03"].values  # green
band_b = r60m["b02"].values  # blue

rgb = np.stack([band_r, band_g, band_b], axis=-1)  # (y, x, 3)
del band_r, band_g, band_b

# 2–98 % percentile linear stretch across all three bands for display contrast
p2, p98 = np.nanpercentile(rgb, 2), np.nanpercentile(rgb, 98)
tci_rgb = np.clip((rgb - p2) / (p98 - p2) * 255, 0, 255).astype(np.uint8)
del rgb

# ---- Compute WGS-84 bounding box from the 60 m coordinate arrays ----
ref_da = r60m["b02"]
x_coords = ref_da.coords["x"].values
y_coords = ref_da.coords["y"].values
half_px_x = abs(float(x_coords[1] - x_coords[0])) / 2
half_px_y = abs(float(y_coords[0] - y_coords[1])) / 2
x_min = float(x_coords.min()) - half_px_x
x_max = float(x_coords.max()) + half_px_x
y_min = float(y_coords.min()) - half_px_y
y_max = float(y_coords.max()) + half_px_y

tci_product_id = parse_sentinel2_product_name(Path(product.href))
tci_epsg = epsg_from_tile_id(tci_product_id.tile_id)
tci_transformer = Transformer.from_crs(f"EPSG:{tci_epsg}", "EPSG:4326", always_xy=True)
tci_corners = [
    tci_transformer.transform(x, y)
    for x, y in [(x_min, y_min), (x_min, y_max), (x_max, y_min), (x_max, y_max)]
]
tci_lons, tci_lats = zip(*tci_corners)
tci_bounds = [[min(tci_lats), min(tci_lons)], [max(tci_lats), max(tci_lons)]]
tci_center = [(min(tci_lats) + max(tci_lats)) / 2, (min(tci_lons) + max(tci_lons)) / 2]

# ---- Folium map ----
tci_map = folium.Map(location=tci_center, zoom_start=9, tiles="CartoDB positron")
folium.raster_layers.ImageOverlay(
    image=tci_rgb,
    bounds=tci_bounds,
    opacity=1.0,
    name="True Colour Composite (B4/B3/B2)",
).add_to(tci_map)
folium.LayerControl().add_to(tci_map)
tci_map
Make this Notebook Trusted to load map: File -> Trust Notebook