diff --git a/docs/source/sentinel2_reference.rst b/docs/source/sentinel2_reference.rst
index 243c5ec..d036262 100644
--- a/docs/source/sentinel2_reference.rst
+++ b/docs/source/sentinel2_reference.rst
@@ -22,7 +22,7 @@ Search Parameters
When searching for Sentinel-2 data, parameters are passed in two ways:
-1. **Direct Parameters:** Passed directly to the ``search()`` method
+1. **Direct Parameters:** Passed directly to the ``query_by_filter()`` method
- ``collection_name`` - Mission/collection identifier
- ``product_type`` - Product type (S2MSI1C, S2MSI2A, etc.)
- ``orbit_direction`` - Orbit direction (ASCENDING/DESCENDING)
@@ -55,13 +55,17 @@ When searching for Sentinel-2 data, parameters are passed in two ways:
.. note::
**Processing Level Filtering**
- ``processingLevel`` as an attribute does not filter correctly. Instead, use ``product_type``
- parameter to specify:
+ Use ``product_type`` for processing-level selection. phidown accepts common aliases
+ such as ``'L1C'``/``'Level-1C'`` and ``'L2A'``/``'Level-2A'``, then sends the
+ canonical CDSE identifiers:
- ``'S2MSI1C'`` for Level-1C (Top-of-Atmosphere reflectance)
- ``'S2MSI2A'`` for Level-2A (Bottom-of-Atmosphere reflectance)
- ``'S2MSI2B'`` for Level-2B (archived format)
+ If ``processingLevel`` or ``productType`` is supplied in ``attributes``,
+ the same Sentinel-2 aliases are normalized, but ``product_type`` is clearer.
+
3. **Method Parameters**
These parameters are passed directly to ``query_by_filter()``, not in the attributes dictionary:
@@ -79,7 +83,8 @@ Use ``'SENTINEL-2'`` as the collection name.
.. code-block:: python
- results = searcher.search(collection_name='SENTINEL-2')
+ searcher.query_by_filter(collection_name='SENTINEL-2')
+ results = searcher.execute_query()
Geographic Parameters
^^^^^^^^^^^^^^^^^^^^^
@@ -92,10 +97,11 @@ Region of Interest defined in Well Known Text (WKT) format with coordinates in d
# Polygon example
aoi_wkt = 'POLYGON((12.4 41.9, 12.5 41.9, 12.5 42.0, 12.4 42.0, 12.4 41.9))'
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
aoi_wkt=aoi_wkt
)
+ results = searcher.execute_query()
Tile Identifier
"""""""""""""""
@@ -104,10 +110,11 @@ Sentinel-2 data is organized in tiles following the Military Grid Reference Syst
.. code-block:: python
# Search for specific tile
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
attributes={'tileId': '32TQM'}
)
+ results = searcher.execute_query()
Product Parameters
^^^^^^^^^^^^^^^^^^
@@ -139,25 +146,28 @@ Sentinel-2 offers various product types:
.. code-block:: python
# Search for Level-1C products
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
product_type='S2MSI1C'
)
+ results = searcher.execute_query()
Processing Level
""""""""""""""""
-Available processing levels:
+Available processing-level product types:
* ``S2MSI1C`` - Level-1C (Top-of-Atmosphere reflectance)
* ``S2MSI2A`` - Level-2A (Bottom-of-Atmosphere reflectance)
+* ``S2MSI2B`` - Level-2B (archived format)
.. code-block:: python
# Search for Level-2A products
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
- attributes={'processingLevel': 'S2MSI2A'}
+ product_type='S2MSI2A'
)
+ results = searcher.execute_query()
Platform Serial Identifier
""""""""""""""""""""""""""
@@ -169,10 +179,11 @@ Sentinel-2 constellation satellites:
.. code-block:: python
# Search for Sentinel-2A data only
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
attributes={'platformSerialIdentifier': 'A'}
)
+ results = searcher.execute_query()
Instrument Short Name
"""""""""""""""""""""
@@ -181,10 +192,11 @@ Instrument Short Name
.. code-block:: python
# Search for MSI instrument data
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
attributes={'instrumentShortName': 'MSI'}
)
+ results = searcher.execute_query()
Sensor Mode
"""""""""""
@@ -197,10 +209,11 @@ Sentinel-2 sensor modes:
.. code-block:: python
# Search for normal observation mode
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
- attributes={'sensorMode': 'INS-NOBS'}
+ attributes={'operationalMode': 'INS-NOBS'}
)
+ results = searcher.execute_query()
Cloud Cover
^^^^^^^^^^^
@@ -236,10 +249,11 @@ Orbit Direction
.. code-block:: python
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
orbit_direction='DESCENDING'
)
+ results = searcher.execute_query()
Orbit Number
""""""""""""
@@ -248,16 +262,18 @@ Absolute orbit number (integer value or range).
.. code-block:: python
# Single orbit
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
attributes={'orbitNumber': '12345'}
)
+ results = searcher.execute_query()
# Orbit range
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
attributes={'orbitNumber': '[12345,12350]'}
)
+ results = searcher.execute_query()
Relative Orbit Number
"""""""""""""""""""""
@@ -266,10 +282,11 @@ Relative orbit number (1-143 for Sentinel-2), representing the orbit within a re
.. code-block:: python
# Search for relative orbit 51
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
attributes={'relativeOrbitNumber': '51'}
)
+ results = searcher.execute_query()
Quality and Processing
^^^^^^^^^^^^^^^^^^^^^^
@@ -281,16 +298,18 @@ Processing baseline version (affects product quality and algorithms used).
.. code-block:: python
# Search for specific processing baseline
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
attributes={'processingBaseline': '04.00'}
)
+ results = searcher.execute_query()
# Search for baseline range
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
attributes={'processingBaseline': '[04.00,05.00]'}
)
+ results = searcher.execute_query()
Status
""""""
@@ -303,10 +322,11 @@ Product availability status:
.. code-block:: python
# Search for immediately available products
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
attributes={'status': 'ONLINE'}
)
+ results = searcher.execute_query()
Mission Take ID
"""""""""""""""
@@ -315,10 +335,11 @@ Mission take identifier for specific acquisition sessions.
.. code-block:: python
# Search for specific mission take
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
attributes={'missionTakeId': 'GS2A_20230601T101030_000123_N04.00'}
)
+ results = searcher.execute_query()
Practical Examples
------------------
@@ -330,8 +351,6 @@ Example 1: Basic Level-1C Search
from phidown import CopernicusDataSearcher
- searcher = CopernicusDataSearcher()
-
# Search for Level-1C products with low cloud cover
searcher = CopernicusDataSearcher()
searcher.query_by_filter(
@@ -357,17 +376,16 @@ Example 2: Level-2A Surface Reflectance
searcher = CopernicusDataSearcher()
# Search for atmospherically corrected Level-2A products
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
product_type='S2MSI2A',
aoi_wkt='POLYGON((12.4 41.9, 12.5 41.9, 12.5 42.0, 12.4 42.0, 12.4 41.9))',
start_date='2023-06-01',
end_date='2023-06-30',
- attributes={
- 'cloudCover': '[0,10]',
- 'processingLevel': 'S2MSI2A'
- }
+ cloud_cover_threshold=10,
+ top=10
)
+ results = searcher.execute_query()
print(f"Found {len(results)} Level-2A products")
@@ -381,16 +399,15 @@ Example 3: Specific Tile Search
searcher = CopernicusDataSearcher()
# Search for specific tile over time
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
product_type='S2MSI1C',
start_date='2023-01-01',
end_date='2023-12-31',
- attributes={
- 'tileId': '32TQM',
- 'cloudCover': '[0,30]'
- }
+ cloud_cover_threshold=30,
+ attributes={'tileId': '32TQM'}
)
+ results = searcher.execute_query()
print(f"Found {len(results)} products for tile 32TQM")
@@ -405,17 +422,18 @@ Example 4: Time Series Analysis
searcher = CopernicusDataSearcher()
# Search for consistent time series data
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
product_type='S2MSI1C',
start_date='2023-01-01',
end_date='2023-12-31',
+ cloud_cover_threshold=20,
attributes={
'tileId': '32TQM',
- 'cloudCover': '[0,20]',
'relativeOrbitNumber': '51'
}
)
+ results = searcher.execute_query()
# Group by date to analyze temporal coverage
results['Date'] = pd.to_datetime(results['ContentDate']).dt.date
@@ -433,29 +451,27 @@ Example 5: Multi-Platform Comparison
searcher = CopernicusDataSearcher()
# Compare data from both Sentinel-2A and Sentinel-2B
- s2a_results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
product_type='S2MSI1C',
aoi_wkt='POLYGON((12.4 41.9, 12.5 41.9, 12.5 42.0, 12.4 42.0, 12.4 41.9))',
start_date='2023-06-01',
end_date='2023-06-30',
- attributes={
- 'platform': 'S2A',
- 'cloudCover': '[0,15]'
- }
+ cloud_cover_threshold=15,
+ attributes={'platformSerialIdentifier': 'A'}
)
+ s2a_results = searcher.execute_query()
- s2b_results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
product_type='S2MSI1C',
aoi_wkt='POLYGON((12.4 41.9, 12.5 41.9, 12.5 42.0, 12.4 42.0, 12.4 41.9))',
start_date='2023-06-01',
end_date='2023-06-30',
- attributes={
- 'platform': 'S2B',
- 'cloudCover': '[0,15]'
- }
+ cloud_cover_threshold=15,
+ attributes={'platformSerialIdentifier': 'B'}
)
+ s2b_results = searcher.execute_query()
print(f"Sentinel-2A: {len(s2a_results)} products")
print(f"Sentinel-2B: {len(s2b_results)} products")
@@ -470,17 +486,16 @@ Example 6: Processing Baseline Filtering
searcher = CopernicusDataSearcher()
# Search for products with latest processing baseline
- results = searcher.search(
+ searcher.query_by_filter(
collection_name='SENTINEL-2',
product_type='S2MSI2A',
aoi_wkt='POLYGON((12.4 41.9, 12.5 41.9, 12.5 42.0, 12.4 42.0, 12.4 41.9))',
start_date='2023-06-01',
end_date='2023-06-30',
- attributes={
- 'processingBaseline': '[04.00,05.00]',
- 'cloudCover': '[0,25]'
- }
+ cloud_cover_threshold=25,
+ attributes={'processingBaseline': '[04.00,05.00]'}
)
+ results = searcher.execute_query()
print(f"Found {len(results)} products with processing baseline 4.00-5.00")
diff --git a/notebooks/1_search_n_download.ipynb b/notebooks/1_search_n_download.ipynb
index a61731e..1a1e424 100644
--- a/notebooks/1_search_n_download.ipynb
+++ b/notebooks/1_search_n_download.ipynb
@@ -22,18 +22,143 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 1,
"id": "10d56e22",
"metadata": {},
"outputs": [
{
- "ename": "",
- "evalue": "",
- "output_type": "error",
- "traceback": [
- "\u001b[1;31mFailed to start the Kernel. \n",
- "\u001b[1;31mView Jupyter log for further details."
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Warning: No AOI (aoi_wkt) provided. Coverage calculation requires an AOI to measure against.\n",
+ "Number of results: 5337\n"
]
+ },
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " Id | \n",
+ " coverage | \n",
+ " Name | \n",
+ " S3Path | \n",
+ " GeoFootprint | \n",
+ " OriginDate | \n",
+ " Attributes | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " 00cd06ea-80e6-4ee2-9fb3-b5c0ec189186 | \n",
+ " None | \n",
+ " S1A_S6_RAW__0SDV_20240502T195132_20240502T1951... | \n",
+ " /eodata/Sentinel-1/SAR/S6_RAW__0S/2024/05/02/S... | \n",
+ " {'type': 'Polygon', 'coordinates': [[[-24.5551... | \n",
+ " 2024-05-02 20:15:12 | \n",
+ " [{'@odata.type': '#OData.CSC.StringAttribute',... | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " e68c7e10-7213-443e-857c-f9ddadb648fb | \n",
+ " None | \n",
+ " S1A_S4_RAW__0SDV_20240502T193657_20240502T1937... | \n",
+ " /eodata/Sentinel-1/SAR/S4_RAW__0S/2024/05/02/S... | \n",
+ " {'type': 'Polygon', 'coordinates': [[[-12.4077... | \n",
+ " 2024-05-02 20:15:29 | \n",
+ " [{'@odata.type': '#OData.CSC.StringAttribute',... | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " 8f52959c-83d4-4568-8b9c-3bdc7933c14b | \n",
+ " None | \n",
+ " S1A_S1_RAW__0SDH_20240502T121147_20240502T1212... | \n",
+ " /eodata/Sentinel-1/SAR/S1_RAW__0S/2024/05/02/S... | \n",
+ " {'type': 'Polygon', 'coordinates': [[[-88.2859... | \n",
+ " 2024-05-02 13:07:28 | \n",
+ " [{'@odata.type': '#OData.CSC.StringAttribute',... | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " 54192b79-3683-4234-82f6-78f154af2d1c | \n",
+ " None | \n",
+ " S1A_S4_RAW__0SDV_20240502T062925_20240502T0629... | \n",
+ " /eodata/Sentinel-1/SAR/S4_RAW__0S/2024/05/02/S... | \n",
+ " {'type': 'Polygon', 'coordinates': [[[-177.858... | \n",
+ " 2024-05-02 07:16:16 | \n",
+ " [{'@odata.type': '#OData.CSC.StringAttribute',... | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " c03e2735-12b5-47cd-9deb-af959af4aa55 | \n",
+ " None | \n",
+ " S1A_S6_RAW__0SDV_20240502T055859_20240502T0559... | \n",
+ " /eodata/Sentinel-1/SAR/S6_RAW__0S/2024/05/02/S... | \n",
+ " {'type': 'Polygon', 'coordinates': [[[-12.6051... | \n",
+ " 2024-05-02 07:16:24 | \n",
+ " [{'@odata.type': '#OData.CSC.StringAttribute',... | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " Id coverage \\\n",
+ "0 00cd06ea-80e6-4ee2-9fb3-b5c0ec189186 None \n",
+ "1 e68c7e10-7213-443e-857c-f9ddadb648fb None \n",
+ "2 8f52959c-83d4-4568-8b9c-3bdc7933c14b None \n",
+ "3 54192b79-3683-4234-82f6-78f154af2d1c None \n",
+ "4 c03e2735-12b5-47cd-9deb-af959af4aa55 None \n",
+ "\n",
+ " Name \\\n",
+ "0 S1A_S6_RAW__0SDV_20240502T195132_20240502T1951... \n",
+ "1 S1A_S4_RAW__0SDV_20240502T193657_20240502T1937... \n",
+ "2 S1A_S1_RAW__0SDH_20240502T121147_20240502T1212... \n",
+ "3 S1A_S4_RAW__0SDV_20240502T062925_20240502T0629... \n",
+ "4 S1A_S6_RAW__0SDV_20240502T055859_20240502T0559... \n",
+ "\n",
+ " S3Path \\\n",
+ "0 /eodata/Sentinel-1/SAR/S6_RAW__0S/2024/05/02/S... \n",
+ "1 /eodata/Sentinel-1/SAR/S4_RAW__0S/2024/05/02/S... \n",
+ "2 /eodata/Sentinel-1/SAR/S1_RAW__0S/2024/05/02/S... \n",
+ "3 /eodata/Sentinel-1/SAR/S4_RAW__0S/2024/05/02/S... \n",
+ "4 /eodata/Sentinel-1/SAR/S6_RAW__0S/2024/05/02/S... \n",
+ "\n",
+ " GeoFootprint OriginDate \\\n",
+ "0 {'type': 'Polygon', 'coordinates': [[[-24.5551... 2024-05-02 20:15:12 \n",
+ "1 {'type': 'Polygon', 'coordinates': [[[-12.4077... 2024-05-02 20:15:29 \n",
+ "2 {'type': 'Polygon', 'coordinates': [[[-88.2859... 2024-05-02 13:07:28 \n",
+ "3 {'type': 'Polygon', 'coordinates': [[[-177.858... 2024-05-02 07:16:16 \n",
+ "4 {'type': 'Polygon', 'coordinates': [[[-12.6051... 2024-05-02 07:16:24 \n",
+ "\n",
+ " Attributes \n",
+ "0 [{'@odata.type': '#OData.CSC.StringAttribute',... \n",
+ "1 [{'@odata.type': '#OData.CSC.StringAttribute',... \n",
+ "2 [{'@odata.type': '#OData.CSC.StringAttribute',... \n",
+ "3 [{'@odata.type': '#OData.CSC.StringAttribute',... \n",
+ "4 [{'@odata.type': '#OData.CSC.StringAttribute',... "
+ ]
+ },
+ "execution_count": 1,
+ "metadata": {},
+ "output_type": "execute_result"
}
],
"source": [
@@ -90,6 +215,181 @@
"searcher.display_results(top_n=5)"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "s2-l2a-section",
+ "metadata": {},
+ "source": [
+ "## Search Sentinel-2 Level-2A products\n",
+ "\n",
+ "Sentinel-2 Level-2A products use the CDSE product identifier `S2MSI2A`. The `query_by_filter()` API also accepts common aliases such as `L2A` or `Level-2A`, but using `S2MSI2A` makes the generated catalogue filter explicit.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "s2-l2a-search",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Number of Sentinel-2 L2A results: 5\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " Id | \n",
+ " coverage | \n",
+ " Name | \n",
+ " S3Path | \n",
+ " GeoFootprint | \n",
+ " OriginDate | \n",
+ " Attributes | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " 0acf34c5-4012-475a-b9fc-ab6facd1e5e4 | \n",
+ " 20.76 | \n",
+ " S2A_MSIL2A_20240524T100031_N0510_R122_T33TUG_2... | \n",
+ " /eodata/Sentinel-2/MSI/L2A/2024/05/24/S2A_MSIL... | \n",
+ " {'type': 'Polygon', 'coordinates': [[[12.56880... | \n",
+ " 2024-05-24 16:59:42 | \n",
+ " [{'@odata.type': '#OData.CSC.StringAttribute',... | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " 7f85053d-4c4f-4443-944e-d3dae8116eae | \n",
+ " 100.00 | \n",
+ " S2A_MSIL2A_20240524T100031_N0510_R122_T33TTG_2... | \n",
+ " /eodata/Sentinel-2/MSI/L2A/2024/05/24/S2A_MSIL... | \n",
+ " {'type': 'Polygon', 'coordinates': [[[11.35495... | \n",
+ " 2024-05-24 16:59:10 | \n",
+ " [{'@odata.type': '#OData.CSC.StringAttribute',... | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " e87a2cbc-de5f-45ed-ac9b-f5ebd208ef9c | \n",
+ " 100.00 | \n",
+ " S2A_MSIL2A_20240524T100031_N0510_R122_T32TQM_2... | \n",
+ " /eodata/Sentinel-2/MSI/L2A/2024/05/24/S2A_MSIL... | \n",
+ " {'type': 'Polygon', 'coordinates': [[[11.43071... | \n",
+ " 2024-05-24 16:59:48 | \n",
+ " [{'@odata.type': '#OData.CSC.StringAttribute',... | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " 6ec6419b-c719-457e-af11-8cfebf83806a | \n",
+ " 20.76 | \n",
+ " S2A_MSIL2A_20240514T100031_N0510_R122_T33TUG_2... | \n",
+ " /eodata/Sentinel-2/MSI/L2A/2024/05/14/S2A_MSIL... | \n",
+ " {'type': 'Polygon', 'coordinates': [[[12.56880... | \n",
+ " 2024-05-14 15:17:10 | \n",
+ " [{'@odata.type': '#OData.CSC.StringAttribute',... | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " 91157ce7-7067-4773-9d4d-84dfeb36f1da | \n",
+ " 20.76 | \n",
+ " S2A_MSIL2A_20240504T100031_N0510_R122_T33TUG_2... | \n",
+ " /eodata/Sentinel-2/MSI/L2A/2024/05/04/S2A_MSIL... | \n",
+ " {'type': 'Polygon', 'coordinates': [[[12.56880... | \n",
+ " 2024-05-04 15:29:11 | \n",
+ " [{'@odata.type': '#OData.CSC.StringAttribute',... | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " Id coverage \\\n",
+ "0 0acf34c5-4012-475a-b9fc-ab6facd1e5e4 20.76 \n",
+ "1 7f85053d-4c4f-4443-944e-d3dae8116eae 100.00 \n",
+ "2 e87a2cbc-de5f-45ed-ac9b-f5ebd208ef9c 100.00 \n",
+ "3 6ec6419b-c719-457e-af11-8cfebf83806a 20.76 \n",
+ "4 91157ce7-7067-4773-9d4d-84dfeb36f1da 20.76 \n",
+ "\n",
+ " Name \\\n",
+ "0 S2A_MSIL2A_20240524T100031_N0510_R122_T33TUG_2... \n",
+ "1 S2A_MSIL2A_20240524T100031_N0510_R122_T33TTG_2... \n",
+ "2 S2A_MSIL2A_20240524T100031_N0510_R122_T32TQM_2... \n",
+ "3 S2A_MSIL2A_20240514T100031_N0510_R122_T33TUG_2... \n",
+ "4 S2A_MSIL2A_20240504T100031_N0510_R122_T33TUG_2... \n",
+ "\n",
+ " S3Path \\\n",
+ "0 /eodata/Sentinel-2/MSI/L2A/2024/05/24/S2A_MSIL... \n",
+ "1 /eodata/Sentinel-2/MSI/L2A/2024/05/24/S2A_MSIL... \n",
+ "2 /eodata/Sentinel-2/MSI/L2A/2024/05/24/S2A_MSIL... \n",
+ "3 /eodata/Sentinel-2/MSI/L2A/2024/05/14/S2A_MSIL... \n",
+ "4 /eodata/Sentinel-2/MSI/L2A/2024/05/04/S2A_MSIL... \n",
+ "\n",
+ " GeoFootprint OriginDate \\\n",
+ "0 {'type': 'Polygon', 'coordinates': [[[12.56880... 2024-05-24 16:59:42 \n",
+ "1 {'type': 'Polygon', 'coordinates': [[[11.35495... 2024-05-24 16:59:10 \n",
+ "2 {'type': 'Polygon', 'coordinates': [[[11.43071... 2024-05-24 16:59:48 \n",
+ "3 {'type': 'Polygon', 'coordinates': [[[12.56880... 2024-05-14 15:17:10 \n",
+ "4 {'type': 'Polygon', 'coordinates': [[[12.56880... 2024-05-04 15:29:11 \n",
+ "\n",
+ " Attributes \n",
+ "0 [{'@odata.type': '#OData.CSC.StringAttribute',... \n",
+ "1 [{'@odata.type': '#OData.CSC.StringAttribute',... \n",
+ "2 [{'@odata.type': '#OData.CSC.StringAttribute',... \n",
+ "3 [{'@odata.type': '#OData.CSC.StringAttribute',... \n",
+ "4 [{'@odata.type': '#OData.CSC.StringAttribute',... "
+ ]
+ },
+ "execution_count": 1,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from phidown.search import CopernicusDataSearcher\n",
+ "\n",
+ "# Example AOI around Rome, Italy. Replace with your own WKT polygon as needed.\n",
+ "s2_l2a_aoi_wkt = (\n",
+ " \"POLYGON((12.35 41.80, 12.65 41.80, 12.65 42.05, \"\n",
+ " \"12.35 42.05, 12.35 41.80))\"\n",
+ ")\n",
+ "\n",
+ "s2_l2a_searcher = CopernicusDataSearcher()\n",
+ "s2_l2a_searcher.query_by_filter(\n",
+ " collection_name=\"SENTINEL-2\",\n",
+ " product_type=\"S2MSI2A\", # Equivalent aliases accepted by phidown: \"L2A\", \"Level-2A\"\n",
+ " aoi_wkt=s2_l2a_aoi_wkt,\n",
+ " start_date=\"2024-05-01T00:00:00\",\n",
+ " end_date=\"2024-05-31T23:59:59\",\n",
+ " cloud_cover_threshold=20,\n",
+ " top=10,\n",
+ ")\n",
+ "\n",
+ "s2_l2a_df = s2_l2a_searcher.execute_query()\n",
+ "print(f\"Number of Sentinel-2 L2A results: {len(s2_l2a_df)}\")\n",
+ "s2_l2a_searcher.display_results(top_n=5)\n"
+ ]
+ },
{
"cell_type": "code",
"execution_count": null,
@@ -136,9 +436,21 @@
],
"metadata": {
"kernelspec": {
- "display_name": "Python 3.12 (py312)",
+ "display_name": ".venv",
"language": "python",
- "name": "py312"
+ "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.11.14"
}
},
"nbformat": 4,
diff --git a/phidown/config.json b/phidown/config.json
index b81712a..018cc6e 100644
--- a/phidown/config.json
+++ b/phidown/config.json
@@ -82,7 +82,7 @@
"productGroupId": null,
"lastOrbitNumber": null,
"operationalMode": null,
- "processingLevel": ["L1C", "L2A"],
+ "processingLevel": ["S2MSI1C", "S2MSI2A", "S2MSI2B"],
"processingCenter": null,
"processorVersion": null,
"granuleIdentifier": null,
@@ -268,4 +268,4 @@
"processorVersion": null
}
}
-}
\ No newline at end of file
+}
diff --git a/phidown/search.py b/phidown/search.py
index 4b41590..e85af06 100755
--- a/phidown/search.py
+++ b/phidown/search.py
@@ -42,6 +42,21 @@
REQUEST_TIMEOUT_SECONDS = 30
PHISAT2_COLLECTION_NAME = "PHISAT-2"
PHISAT2_COLLECTION_ALIASES = {"PHISAT", "PHISAT2", "PHISAT-2", "PHISAT_2"}
+SENTINEL2_COLLECTION_NAME = "SENTINEL-2"
+_SENTINEL2_LEVEL_ALIASES = {
+ "S2MSI1C": "S2MSI1C",
+ "L1C": "S2MSI1C",
+ "1C": "S2MSI1C",
+ "LEVEL1C": "S2MSI1C",
+ "S2MSI2A": "S2MSI2A",
+ "L2A": "S2MSI2A",
+ "2A": "S2MSI2A",
+ "LEVEL2A": "S2MSI2A",
+ "S2MSI2B": "S2MSI2B",
+ "L2B": "S2MSI2B",
+ "2B": "S2MSI2B",
+ "LEVEL2B": "S2MSI2B",
+}
_SUPPORTED_AOI_WKT_TYPES = (
"POINT",
"MULTIPOINT",
@@ -323,6 +338,11 @@ def _extract_date_start(value: typing.Any) -> typing.Any:
return value
+def _normalize_sentinel2_level_alias(value: str) -> str:
+ normalized = re.sub(r"[\s_-]+", "", value).upper()
+ return _SENTINEL2_LEVEL_ALIASES.get(normalized, value)
+
+
class CopernicusDataSearcher:
def __init__(
self,
@@ -477,7 +497,7 @@ def query_by_filter(
if not self.burst_mode:
self._validate_collection(self.collection_name) # Validate collection name only in non-burst mode
- self.product_type = product_type
+ self.product_type = self._normalize_product_type(product_type)
if not self.burst_mode:
self._validate_product_type() # Validate product type (depends on collection_name and config)
@@ -500,7 +520,7 @@ def query_by_filter(
self.order_by = order_by
self._validate_order_by()
- self.attributes = attributes
+ self.attributes = self._normalize_attributes(attributes)
if self.attributes is not None and not self.burst_mode:
self._validate_attributes()
@@ -556,6 +576,28 @@ def _is_phisat2_collection(self, collection_name: typing.Optional[str] = None) -
candidate = self.collection_name if collection_name is None else collection_name
return isinstance(candidate, str) and candidate.upper() in PHISAT2_COLLECTION_ALIASES
+ def _is_sentinel2_collection(self) -> bool:
+ return self.collection_name == SENTINEL2_COLLECTION_NAME
+
+ def _normalize_product_type(self, product_type: typing.Optional[str]) -> typing.Optional[str]:
+ if self._is_sentinel2_collection() and isinstance(product_type, str):
+ return _normalize_sentinel2_level_alias(product_type)
+ return product_type
+
+ def _normalize_attributes(
+ self,
+ attributes: typing.Optional[typing.Dict[str, typing.Union[str, int, float]]],
+ ) -> typing.Optional[typing.Dict[str, typing.Union[str, int, float]]]:
+ if not self._is_sentinel2_collection() or attributes is None:
+ return attributes
+
+ normalized_attributes = dict(attributes)
+ for key in ("productType", "processingLevel"):
+ value = normalized_attributes.get(key)
+ if isinstance(value, str):
+ normalized_attributes[key] = _normalize_sentinel2_level_alias(value)
+ return normalized_attributes
+
def _phisat2_filter_from_inputs(
self,
*,
diff --git a/pyproject.toml b/pyproject.toml
index ceb8ed2..820c5c0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "phidown"
-version = "0.1.27"
+version = "0.1.29"
description = "Search and download Copernicus Data Space and PhiSat-2 data with a pythonic interface."
authors = [
{ name = "Roberto Del Prete", email = "roberto.delprete@esa.int; robertodelprete88@gmail.com" }
diff --git a/tests/test_sentinel2_level_aliases.py b/tests/test_sentinel2_level_aliases.py
new file mode 100644
index 0000000..18b3719
--- /dev/null
+++ b/tests/test_sentinel2_level_aliases.py
@@ -0,0 +1,75 @@
+import os
+
+import pytest
+
+from phidown.search import CopernicusDataSearcher
+
+
+CONFIG_PATH = os.path.join(os.path.dirname(__file__), "..", "phidown", "config.json")
+
+
+@pytest.mark.parametrize("alias", ["L1C", "Level-1C"])
+def test_sentinel2_product_type_accepts_level_1c_aliases(alias):
+ searcher = CopernicusDataSearcher(
+ config_path=CONFIG_PATH,
+ collection_name="SENTINEL-2",
+ product_type=alias,
+ )
+
+ searcher._build_filter()
+
+ assert searcher.product_type == "S2MSI1C"
+ assert "Value eq 'S2MSI1C'" in searcher.filter_condition
+ assert alias not in searcher.filter_condition
+
+
+def test_sentinel2_product_type_accepts_level_2a_alias():
+ searcher = CopernicusDataSearcher(
+ config_path=CONFIG_PATH,
+ collection_name="SENTINEL-2",
+ product_type="Level-2A",
+ )
+
+ searcher._build_filter()
+
+ assert searcher.product_type == "S2MSI2A"
+ assert "Value eq 'S2MSI2A'" in searcher.filter_condition
+ assert "Level-2A" not in searcher.filter_condition
+
+
+def test_sentinel2_processing_level_attribute_uses_canonical_cdse_value():
+ searcher = CopernicusDataSearcher(
+ config_path=CONFIG_PATH,
+ collection_name="SENTINEL-2",
+ attributes={"processingLevel": "L2A"},
+ )
+
+ searcher._build_filter()
+
+ assert searcher.attributes == {"processingLevel": "S2MSI2A"}
+ assert "att/Name eq 'processingLevel'" in searcher.filter_condition
+ assert "Value eq 'S2MSI2A'" in searcher.filter_condition
+ assert "Value eq 'L2A'" not in searcher.filter_condition
+
+
+def test_sentinel2_product_type_attribute_uses_canonical_cdse_value():
+ searcher = CopernicusDataSearcher(
+ config_path=CONFIG_PATH,
+ collection_name="SENTINEL-2",
+ attributes={"productType": "L1C"},
+ )
+
+ searcher._build_filter()
+
+ assert searcher.attributes == {"productType": "S2MSI1C"}
+ assert "att/Name eq 'productType'" in searcher.filter_condition
+ assert "Value eq 'S2MSI1C'" in searcher.filter_condition
+
+
+def test_non_sentinel2_product_type_does_not_accept_sentinel2_alias():
+ with pytest.raises(ValueError, match="Invalid product type: L2A"):
+ CopernicusDataSearcher(
+ config_path=CONFIG_PATH,
+ collection_name="SENTINEL-1",
+ product_type="L2A",
+ )