Skip to content
Open
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
33 changes: 33 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Tests

on:
push:
branches: [main]
pull_request:

jobs:
pytest:
runs-on: ubuntu-latest
strategy:
fail-fast: false
# pyproject.toml declares requires-python >=3.11 and lists 3.11 and 3.12 in
# its classifiers, so PyPI users can install on either. Local development
# usually runs 3.12, so testing 3.11 here catches code that breaks on the
# oldest supported version. The jobs run in parallel and the suite is fast,
# so the second version adds little CI time.
matrix:
python-version: ["3.11", "3.12"]
steps:
- name: Checkout openeo-udp
uses: actions/checkout@v4

- name: Set up uv
uses: astral-sh/setup-uv@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync --group dev

- name: Run tests
run: uv run pytest
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ Before writing any code, thoroughly analyze the original evalscript:
- **The openEO UDP parameter management system** (`from openeo_udp import ParameterManager`)

- **Set up parameter management** to support multiple locations and backends:
- Create a `.params.py` file alongside your notebook with parameter sets for different locations
- Create a `<algorithm_id>.params.py` file in `openeo_udp/params/` with parameter sets for different locations. File names must be unique, because notebooks load them by name
- Use the parameter manager for flexible backend connections and parameter handling
- Follow the five practices below consistently across all notebooks

Expand All @@ -190,8 +190,8 @@ is the canonical reference.

##### Practice 1 — Initialize and display options

In the imports cell, initialize the `ParameterManager` with the co-located
`.params.py` file and call `print_options()` so contributors can see available
In the imports cell, initialize the `ParameterManager` with the name of the
algorithm's `.params.py` file in `openeo_udp/params/` and call `print_options()` so contributors can see available
parameter sets and endpoints at a glance:

```python
Expand Down
8 changes: 4 additions & 4 deletions LICENSING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ Conversion: Development Seed (openEO-UDP project)
- `notebooks/sentinel/sentinel-2/fire_and_disaster_monitoring/bais2_burned_area.ipynb`
- `notebooks/sentinel/sentinel-2/marine_and_water_bodies/ndci_cyanobacteria.ipynb`
- `notebooks/sentinel/sentinel-2/advanced_spectral_processing/cab_chlorophyll_content.ipynb`
- `notebooks/sentinel/sentinel-2/advanced_spectral_processing/cab_chlorophyll_content.params.py`
- `openeo_udp/params/cab_chlorophyll_content.params.py`
- `notebooks/sentinel/sentinel-2/advanced_spectral_processing/ndvi_anomaly_detection.ipynb`
- `notebooks/sentinel/sentinel-2/advanced_spectral_processing/ndvi_anomaly_detection.params.py`
- `openeo_udp/params/ndvi_anomaly_detection.params.py`
- `notebooks/sentinel/sentinel-3/land_surface_temperature.ipynb`
- `notebooks/sentinel/sentinel-3/land_surface_temperature.params.py`
- `openeo_udp/params/land_surface_temperature.params.py`
- `notebooks/sentinel/sentinel-3/olci_under_s5.ipynb`
- `notebooks/sentinel/sentinel-3/olci_under_s5.params.py`
- `openeo_udp/params/olci_under_s5.params.py`
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,25 @@ When creating or opening a notebook:
3. Execute the cells sequentially to see the conversion process, validate results, and export the UDP definition

**Parameter Management Integration:**
Notebooks include parameter files (`.params.py`) that define multiple locations and configurations. You can:
Each notebook has a parameter file (`.params.py`) that defines multiple locations and configurations. The parameter files ship with the `openeo-udp` package in [`openeo_udp/params/`](openeo_udp/params/) (except for the self-contained machine-learning notebooks, which keep theirs in their own folder), so `ParameterManager("<algorithm>.params.py")` finds them wherever the package is installed. You can:

- Use interactive widgets to select parameters and endpoints via dropdowns
- Switch between locations (Venice Lagoon, Lake Victoria, etc.) without editing code
- Connect to different OpenEO backends with automatic parameter mapping
- Run the same algorithm on different areas by simply changing parameter sets

**Using your own parameter file:**
Users and developers can override a packaged parameter file without changing the package. Create a file with the same name in the notebook's folder (the notebook's working directory), and `ParameterManager` loads it instead of the packaged one. The easiest way is to copy the packaged file and edit it:

```python
import shutil
from importlib.resources import files

shutil.copy(files("openeo_udp.params") / "urban_areas.params.py", "urban_areas.params.py")
```

Then edit `urban_areas.params.py`, for example to add a parameter set for your own area and dates, and re-run the notebook from the `ParameterManager` cell. `print_options()` shows which file was loaded and warns when a local file overrides the packaged one; delete or rename the local file to go back to the packaged version. You can also keep your file under a different name and pass its path, e.g. `ParameterManager("my_urban_areas.params.py")`.

Most notebooks are designed to run completely from top to bottom without modification, though you may want to adjust spatial extents or temporal ranges to explore different areas.

## Using the Exported UDPs
Expand Down
10 changes: 9 additions & 1 deletion docs/parameter-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,17 @@ time_dimension: 'time'
### Initialization

```python
param_manager = ParameterManager('path/to/params.py')
# Bare file name: loaded from openeo_udp/params/ in the installed package
param_manager = ParameterManager('algorithm.params.py')

# Explicit path: loaded from disk (a local file with the same name as a
# packaged one takes precedence)
param_manager = ParameterManager('path/to/my_algorithm.params.py')
```

Use `openeo_udp.list_parameter_files()` to see which parameter files ship with
the package.

### Core Methods

#### Parameter Set Management
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Extra packages for machine_learning.ipynb, on top of openeo-udp and its
# notebooks extra:
# pip install "openeo-udp[notebooks]" -r requirements.txt
geopandas>=1.1.4
netcdf4>=1.7.4
scikit-learn>=1.9.0
shapely>=2.0.0
xarray>=2025.1.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Extra packages for dnbr_vegetation_classification_udf.ipynb, on top of
# openeo-udp and its notebooks extra:
# pip install "openeo-udp[notebooks]" -r requirements.txt
geopandas>=1.1.4
rasterio>=1.4.0
scikit-learn>=1.9.0
scipy>=1.17.1
xarray>=2025.1.0
5 changes: 3 additions & 2 deletions openeo_udp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
for OpenEO notebooks in the DevelopmentSeed UDP repository.
"""

__version__ = "0.1.0"
__version__ = "0.2.0"

from .parameter_manager import ParameterManager
from .parameter_manager import ParameterManager, list_parameter_files

__all__ = [
"ParameterManager",
"list_parameter_files",
]
66 changes: 60 additions & 6 deletions openeo_udp/parameter_manager.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,85 @@
"""Simple parameter manager for OpenEO UDP notebooks.

This module provides a simplified ParameterManager that loads parameter sets
from co-located .py files containing a get_parameters() function.
from .params.py files containing a get_parameters() function. The parameter
files for all algorithms ship with the package in ``openeo_udp/params/``.
"""

import importlib.util
from importlib import resources
from pathlib import Path
from typing import Any, Dict, List

from openeo.api.process import Parameter


def _resolve_param_file(param_file_path: str | Path) -> Path:
"""Locate a parameter file on disk or inside the installed package.

A path that exists on disk always wins, so a local file can override the
packaged one. A bare file name (no directory part) that does not exist
locally is looked up in the ``openeo_udp.params`` package.
"""
path = Path(param_file_path)
if path.exists():
return path

if path.parent == Path("."):
packaged = resources.files("openeo_udp.params") / path.name
if packaged.is_file():
return Path(str(packaged))

return path # Missing; _load_parameter_sets raises FileNotFoundError


def list_parameter_files() -> List[str]:
"""Return the names of the parameter files shipped with openeo_udp."""
return sorted(
entry.name
for entry in resources.files("openeo_udp.params").iterdir()
if entry.name.endswith(".params.py")
)


class ParameterManager:
"""Simple parameter manager for OpenEO UDP notebooks.

Loads parameter sets from co-located .py files that return
pre-configured openEO Parameter objects.
Loads parameter sets from .params.py files that return pre-configured
openEO Parameter objects.
"""

def __init__(self, param_file_path: str):
def __init__(self, param_file_path: str | Path):
"""Initialize parameter manager with a parameter file.

Args:
param_file_path: Path to the .params.py file
param_file_path: Path to a .params.py file, or the bare file name
of one shipped with the package (e.g. ``"urban_areas.params.py"``).
A file at the given path takes precedence over the packaged one.
"""
self.param_file = Path(param_file_path)
self.param_file = _resolve_param_file(param_file_path)
self.is_packaged = self._is_packaged_file(self.param_file)
self._parameter_sets = self._load_parameter_sets()
self._current_set = None

@staticmethod
def _is_packaged_file(path: Path) -> bool:
"""Return True if ``path`` is a parameter file shipped in openeo_udp.params."""
packaged_dir = Path(str(resources.files("openeo_udp.params"))).resolve()
return path.resolve().parent == packaged_dir

def describe_source(self) -> str:
"""Describe which parameter file was loaded and where it came from."""
if self.is_packaged:
return f"{self.param_file.name} (packaged with openeo-udp)"

description = f"{self.param_file.resolve()} (local file)"
if self.param_file.name in list_parameter_files():
description += (
f"\n ⚠️ This local file overrides the packaged "
f"{self.param_file.name}. Delete or rename it to use the packaged one."
)
return description

def _load_parameter_sets(self) -> Dict[str, Dict[str, Any]]:
"""Load parameter sets from the .py file.

Expand Down Expand Up @@ -175,6 +227,8 @@ def print_options(self, algorithm_name: str = "algorithm") -> None:
"""
from .endpoints import get_all_endpoints

print(f"Parameter file: {self.describe_source()}\n")

# Show available parameter sets
available_sets = self.list_parameter_sets()
print(f"Available parameter sets for {algorithm_name}:")
Expand Down
4 changes: 4 additions & 0 deletions openeo_udp/params/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Parameter files (``*.params.py``) for the openEO UDP notebooks.

Load one by file name with ``ParameterManager("<algorithm>.params.py")``.
"""
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Parameter definitions for the Fraction of Absorbed Photosynthetically Active
"""Parameter definitions for the Fraction of Absorbed Photosynthetically Active
Radiation (FAPAR) calculation.

This file defines parameter sets that can be used with the FAPAR algorithm
Expand Down Expand Up @@ -53,31 +53,37 @@ def get_parameters():
"bounding_box": Parameter(
"bounding_box",
description="Spatial extent for Cropland Plain, Croatia",
schema={"type": "object", "subtype": "bounding-box"},
default={"west": 14.09, "south": 45.174, "east": 14.27, "north": 45.25},
),
"time": Parameter(
"time",
description="Temporal range for data acquisition",
schema={"type": "array", "subtype": "temporal-interval"},
default=["2025-05-10", "2025-05-12"],
),
"bands": Parameter(
"bands",
description="Sentinel-2 bands required for FAPAR calculation",
schema={"type": "array", "subtype": "band-names"},
default=fapar_bands,
),
"collection": Parameter(
"collection",
description="Data collection identifier",
schema={"type": "string", "subtype": "collection-id"},
default="sentinel-2-l2a",
),
"cloud_cover": Parameter(
"cloud_cover",
description="Maximum cloud cover percentage",
schema={"type": "number"},
default=30,
),
"reflectance_scale": Parameter(
"reflectance_scale",
description="Scale factor to convert band values to 0-1 reflectance (10000.0 for integer-scaled L2A, 1.0 for endpoints that already return reflectance)",
schema={"type": "number"},
default=10000.0,
),
},
Expand All @@ -86,31 +92,37 @@ def get_parameters():
"bounding_box": Parameter(
"bounding_box",
description="Spatial extent for Natural Parks in Provence-Alpes, France",
schema={"type": "object", "subtype": "bounding-box"},
default={"west": 3.09, "south": 43.82, "east": 4.80, "north": 44.57},
),
"time": Parameter(
"time",
description="Temporal range for data acquisition",
schema={"type": "array", "subtype": "temporal-interval"},
default=["2025-08-01", "2025-08-03"],
),
"bands": Parameter(
"bands",
description="Sentinel-2 bands required for FAPAR calculation",
schema={"type": "array", "subtype": "band-names"},
default=fapar_bands,
),
"collection": Parameter(
"collection",
description="Data collection identifier",
schema={"type": "string", "subtype": "collection-id"},
default="sentinel-2-l2a",
),
"cloud_cover": Parameter(
"cloud_cover",
description="Maximum cloud cover percentage",
schema={"type": "number"},
default=30,
),
"reflectance_scale": Parameter(
"reflectance_scale",
description="Scale factor to convert band values to 0-1 reflectance (10000.0 for integer-scaled L2A, 1.0 for endpoints that already return reflectance)",
schema={"type": "number"},
default=10000.0,
),
},
Expand All @@ -119,31 +131,37 @@ def get_parameters():
"bounding_box": Parameter(
"bounding_box",
description="Spatial extent for a Natural Reserve in Rome, Italy",
schema={"type": "object", "subtype": "bounding-box"},
default={"west": 12.09, "south": 41.78, "east": 12.39, "north": 41.92},
),
"time": Parameter(
"time",
description="Temporal range for data acquisition",
schema={"type": "array", "subtype": "temporal-interval"},
default=["2025-10-06", "2025-10-08"],
),
"bands": Parameter(
"bands",
description="Sentinel-2 bands required for FAPAR calculation",
schema={"type": "array", "subtype": "band-names"},
default=fapar_bands,
),
"collection": Parameter(
"collection",
description="Data collection identifier",
schema={"type": "string", "subtype": "collection-id"},
default="sentinel-2-l2a",
),
"cloud_cover": Parameter(
"cloud_cover",
description="Maximum cloud cover percentage",
schema={"type": "number"},
default=30,
),
"reflectance_scale": Parameter(
"reflectance_scale",
description="Scale factor to convert band values to 0-1 reflectance (10000.0 for integer-scaled L2A, 1.0 for endpoints that already return reflectance)",
schema={"type": "number"},
default=10000.0,
),
},
Expand Down
Loading
Loading