eoio 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eoio/__init__.py +20 -0
- eoio/_version.py +21 -0
- eoio/deps.py +67 -0
- eoio/interface.py +300 -0
- eoio/processors/__init__.py +4 -0
- eoio/processors/add_lat_lon/__init__.py +4 -0
- eoio/processors/add_lat_lon/processor.py +203 -0
- eoio/processors/add_lat_lon/tests/__init__.py +0 -0
- eoio/processors/add_lat_lon/tests/test_processor.py +168 -0
- eoio/processors/interpolate/__init__.py +4 -0
- eoio/processors/interpolate/processor.py +454 -0
- eoio/processors/interpolate/tests/__init__.py +0 -0
- eoio/processors/interpolate/tests/test_processor.py +168 -0
- eoio/processors/processor_pipeline.py +147 -0
- eoio/processors/registry.py +34 -0
- eoio/processors/s2_rut/__init__.py +4 -0
- eoio/processors/s2_rut/processor.py +176 -0
- eoio/processors/s2_rut/tests/__init__.py +0 -0
- eoio/processors/s2_rut/tests/test_processor.py +131 -0
- eoio/processors/tests/__init__.py +0 -0
- eoio/processors/tests/test_processor_pipeline.py +260 -0
- eoio/processors/units/__init__.py +4 -0
- eoio/processors/units/drivers/__init__.py +4 -0
- eoio/processors/units/drivers/base.py +97 -0
- eoio/processors/units/drivers/registry.py +34 -0
- eoio/processors/units/drivers/s2lmsi1c.py +99 -0
- eoio/processors/units/drivers/tests/__init__.py +0 -0
- eoio/processors/units/processor.py +273 -0
- eoio/processors/units/tests/__init__.py +0 -0
- eoio/readers/__init__.py +18 -0
- eoio/readers/airbus_pleiades/__init__.py +0 -0
- eoio/readers/airbus_pleiades/aux_data.py +181 -0
- eoio/readers/airbus_pleiades/conventions.py +36 -0
- eoio/readers/airbus_pleiades/data_io.py +146 -0
- eoio/readers/airbus_pleiades/layout.py +86 -0
- eoio/readers/airbus_pleiades/metadata.py +359 -0
- eoio/readers/airbus_pleiades/reader.py +166 -0
- eoio/readers/airbus_pleiades/tests/__init__.py +0 -0
- eoio/readers/airbus_pleiades/tests/test_aux_data.py +228 -0
- eoio/readers/airbus_pleiades/tests/test_conventions.py +34 -0
- eoio/readers/airbus_pleiades/tests/test_data_io.py +256 -0
- eoio/readers/airbus_pleiades/tests/test_layout.py +188 -0
- eoio/readers/airbus_pleiades/tests/test_metadata.py +352 -0
- eoio/readers/airbus_pleiades/tests/test_reader.py +223 -0
- eoio/readers/base.py +358 -0
- eoio/readers/emit/__init__.py +0 -0
- eoio/readers/emit/angles.py +59 -0
- eoio/readers/emit/aux_data.py +143 -0
- eoio/readers/emit/data_io.py +131 -0
- eoio/readers/emit/layout.py +117 -0
- eoio/readers/emit/metadata.py +142 -0
- eoio/readers/emit/reader.py +149 -0
- eoio/readers/emit/subset.py +82 -0
- eoio/readers/emit/tests/__init__.py +0 -0
- eoio/readers/emit/tests/test_angles.py +44 -0
- eoio/readers/emit/tests/test_aux_data.py +38 -0
- eoio/readers/emit/tests/test_data_io.py +26 -0
- eoio/readers/emit/tests/test_layout.py +64 -0
- eoio/readers/emit/tests/test_metadata.py +86 -0
- eoio/readers/emit/tests/test_reader.py +43 -0
- eoio/readers/emit/tests/test_subset.py +51 -0
- eoio/readers/era5/reader.py +114 -0
- eoio/readers/era5/subset.py +33 -0
- eoio/readers/factory.py +131 -0
- eoio/readers/generic_netcdf/__init__.py +0 -0
- eoio/readers/generic_netcdf/data_io.py +71 -0
- eoio/readers/generic_netcdf/metadata.py +126 -0
- eoio/readers/generic_netcdf/reader.py +94 -0
- eoio/readers/generic_netcdf/subset.py +83 -0
- eoio/readers/generic_netcdf/tests/__init__.py +0 -0
- eoio/readers/generic_netcdf/tests/test_data_io.py +33 -0
- eoio/readers/generic_netcdf/tests/test_metadata.py +30 -0
- eoio/readers/generic_netcdf/tests/test_reader.py +34 -0
- eoio/readers/generic_netcdf/tests/test_subset.py +28 -0
- eoio/readers/hypernets/__init__.py +21 -0
- eoio/readers/hypernets/data_io.py +54 -0
- eoio/readers/hypernets/metadata.py +129 -0
- eoio/readers/hypernets/reader.py +324 -0
- eoio/readers/hypernets/subset.py +152 -0
- eoio/readers/hypernets/tests/__init__.py +0 -0
- eoio/readers/hypernets/tests/test_data_io.py +43 -0
- eoio/readers/hypernets/tests/test_metadata.py +50 -0
- eoio/readers/hypernets/tests/test_reader.py +195 -0
- eoio/readers/hypernets/tests/test_reader_end_to_end.py +204 -0
- eoio/readers/hypernets/tests/test_subset.py +130 -0
- eoio/readers/landsat/__init__.py +1 -0
- eoio/readers/landsat/aux_data/__init__.py +1 -0
- eoio/readers/landsat/aux_data/angles.py +97 -0
- eoio/readers/landsat/aux_data/aux_data.py +70 -0
- eoio/readers/landsat/aux_data/tests/__init__.py +0 -0
- eoio/readers/landsat/aux_data/tests/test_angles.py +105 -0
- eoio/readers/landsat/aux_data/tests/test_aux.py +56 -0
- eoio/readers/landsat/conventions.py +39 -0
- eoio/readers/landsat/data_io.py +157 -0
- eoio/readers/landsat/layout.py +156 -0
- eoio/readers/landsat/metadata/__init__.py +54 -0
- eoio/readers/landsat/metadata/extractor.py +372 -0
- eoio/readers/landsat/metadata/ls_mtd_json.py +176 -0
- eoio/readers/landsat/metadata/ls_mtd_xml.py +328 -0
- eoio/readers/landsat/metadata/tests/__init__.py +1 -0
- eoio/readers/landsat/metadata/tests/test_extractor.py +159 -0
- eoio/readers/landsat/metadata/tests/test_ls_mtd_json.py +114 -0
- eoio/readers/landsat/metadata/tests/test_ls_mtd_xml.py +138 -0
- eoio/readers/landsat/reader.py +210 -0
- eoio/readers/landsat/tests/__init__.py +0 -0
- eoio/readers/landsat/tests/test_conventions.py +33 -0
- eoio/readers/landsat/tests/test_data_io.py +123 -0
- eoio/readers/landsat/tests/test_layout.py +125 -0
- eoio/readers/landsat/tests/test_reader.py +91 -0
- eoio/readers/metadata.py +201 -0
- eoio/readers/planetscope/__init__.py +1 -0
- eoio/readers/planetscope/aux_data.py +103 -0
- eoio/readers/planetscope/conventions.py +37 -0
- eoio/readers/planetscope/data_io.py +143 -0
- eoio/readers/planetscope/layout.py +93 -0
- eoio/readers/planetscope/metadata.py +336 -0
- eoio/readers/planetscope/reader.py +193 -0
- eoio/readers/planetscope/tests/__init__.py +0 -0
- eoio/readers/planetscope/tests/test_aux_data.py +155 -0
- eoio/readers/planetscope/tests/test_conventions.py +36 -0
- eoio/readers/planetscope/tests/test_data_io.py +218 -0
- eoio/readers/planetscope/tests/test_layout.py +143 -0
- eoio/readers/planetscope/tests/test_metadata.py +296 -0
- eoio/readers/planetscope/tests/test_reader.py +230 -0
- eoio/readers/radcalnet/__init__.py +0 -0
- eoio/readers/radcalnet/data_io.py +98 -0
- eoio/readers/radcalnet/metadata.py +134 -0
- eoio/readers/radcalnet/reader.py +190 -0
- eoio/readers/radcalnet/subset.py +144 -0
- eoio/readers/radcalnet/tests/__init__.py +0 -0
- eoio/readers/radcalnet/tests/test_data_io.py +48 -0
- eoio/readers/radcalnet/tests/test_reader.py +382 -0
- eoio/readers/radcalnet/tests/test_subset.py +180 -0
- eoio/readers/radcalnet/utils.py +31 -0
- eoio/readers/sentinel2/__init__.py +0 -0
- eoio/readers/sentinel2/aux_vars/__init__.py +0 -0
- eoio/readers/sentinel2/aux_vars/angles.py +101 -0
- eoio/readers/sentinel2/aux_vars/aux_data.py +79 -0
- eoio/readers/sentinel2/aux_vars/masks.py +18 -0
- eoio/readers/sentinel2/aux_vars/meteo.py +83 -0
- eoio/readers/sentinel2/aux_vars/tests/__init__.py +0 -0
- eoio/readers/sentinel2/aux_vars/tests/test_angles.py +124 -0
- eoio/readers/sentinel2/aux_vars/tests/test_aux.py +216 -0
- eoio/readers/sentinel2/aux_vars/tests/test_masks.py +30 -0
- eoio/readers/sentinel2/aux_vars/tests/test_meteo.py +317 -0
- eoio/readers/sentinel2/conventions.py +26 -0
- eoio/readers/sentinel2/data_io.py +232 -0
- eoio/readers/sentinel2/layout.py +680 -0
- eoio/readers/sentinel2/metadata/__init__.py +0 -0
- eoio/readers/sentinel2/metadata/extractor.py +282 -0
- eoio/readers/sentinel2/metadata/s2_ds_mtd.py +85 -0
- eoio/readers/sentinel2/metadata/s2_prod_mtd.py +368 -0
- eoio/readers/sentinel2/metadata/s2_tl_mtd.py +386 -0
- eoio/readers/sentinel2/metadata/tests/__init__.py +0 -0
- eoio/readers/sentinel2/metadata/tests/test_extractor.py +473 -0
- eoio/readers/sentinel2/metadata/tests/test_s2_ds_mtd.py +133 -0
- eoio/readers/sentinel2/metadata/tests/test_s2_prod_mtd.py +206 -0
- eoio/readers/sentinel2/metadata/tests/test_s2_tl_mtd.py +299 -0
- eoio/readers/sentinel2/metadata/var_names.py +104 -0
- eoio/readers/sentinel2/reader.py +156 -0
- eoio/readers/sentinel2/tests/__init__.py +0 -0
- eoio/readers/sentinel2/tests/test_conventions.py +55 -0
- eoio/readers/sentinel2/tests/test_data_io.py +472 -0
- eoio/readers/sentinel2/tests/test_layout.py +325 -0
- eoio/readers/sentinel2/tests/test_layout_l2a.py +69 -0
- eoio/readers/sentinel2/tests/test_reader.py +236 -0
- eoio/readers/sentinel3_olci/__init__.py +5 -0
- eoio/readers/sentinel3_olci/auxiliary.py +651 -0
- eoio/readers/sentinel3_olci/conventions.py +6 -0
- eoio/readers/sentinel3_olci/data_io.py +349 -0
- eoio/readers/sentinel3_olci/layout.py +229 -0
- eoio/readers/sentinel3_olci/masks.py +146 -0
- eoio/readers/sentinel3_olci/metadata/extractor.py +263 -0
- eoio/readers/sentinel3_olci/metadata/s3_olci_mtd.py +373 -0
- eoio/readers/sentinel3_olci/metadata/tests/__init__.py +1 -0
- eoio/readers/sentinel3_olci/metadata/tests/test_extractor.py +335 -0
- eoio/readers/sentinel3_olci/metadata/tests/test_s3_olci_mtd.py +308 -0
- eoio/readers/sentinel3_olci/metadata/var_names.py +33 -0
- eoio/readers/sentinel3_olci/reader.py +260 -0
- eoio/readers/sentinel3_olci/tests/__init__.py +0 -0
- eoio/readers/sentinel3_olci/tests/test_auxiliary.py +133 -0
- eoio/readers/sentinel3_olci/tests/test_conventions.py +17 -0
- eoio/readers/sentinel3_olci/tests/test_data_io.py +254 -0
- eoio/readers/sentinel3_olci/tests/test_layout.py +205 -0
- eoio/readers/sentinel3_olci/tests/test_masks.py +195 -0
- eoio/readers/sentinel3_olci/tests/test_reader.py +273 -0
- eoio/readers/subset/__init__.py +0 -0
- eoio/readers/subset/angle_subset.py +180 -0
- eoio/readers/subset/base_subset.py +349 -0
- eoio/readers/subset/datetime_subset.py +146 -0
- eoio/readers/subset/roi_subset.py +460 -0
- eoio/readers/subset/tests/__init__.py +0 -0
- eoio/readers/subset/tests/test_roi_subset.py +148 -0
- eoio/readers/subset/tests/test_wavelength_subset.py +161 -0
- eoio/readers/subset/time_of_day_subset.py +163 -0
- eoio/readers/subset/wavelength_subset.py +53 -0
- eoio/readers/tests/__init__.py +0 -0
- eoio/readers/tests/test_base.py +216 -0
- eoio/readers/tests/test_factory.py +83 -0
- eoio/readers/tests/test_metadata.py +212 -0
- eoio/readers/tests/test_xml.py +398 -0
- eoio/readers/xml.py +407 -0
- eoio/tests/__init__.py +0 -0
- eoio/tests/test_deps.py +58 -0
- eoio/tests/test_interface.py +76 -0
- eoio/utils/__init__.py +0 -0
- eoio/utils/crs_utils.py +199 -0
- eoio/utils/dict_tools.py +751 -0
- eoio/utils/formatters.py +171 -0
- eoio/utils/instrument_rsr.py +95 -0
- eoio/utils/jp2_tools.py +86 -0
- eoio/utils/rasterio_utils.py +82 -0
- eoio/utils/read_utils.py +289 -0
- eoio/utils/tests/__init__.py +0 -0
- eoio/utils/tests/test_crs_utils.py +168 -0
- eoio/utils/tests/test_dict_tools.py +932 -0
- eoio/utils/tests/test_formatters.py +134 -0
- eoio/utils/tests/test_instrument_rsr.py +52 -0
- eoio/utils/tests/test_jp2_tools.py +98 -0
- eoio/utils/tests/test_read_utils.py +533 -0
- eoio/utils/tests/test_tif_tools.py +140 -0
- eoio/utils/tif_tools.py +78 -0
- eoio-0.1.2.dist-info/METADATA +99 -0
- eoio-0.1.2.dist-info/RECORD +227 -0
- eoio-0.1.2.dist-info/WHEEL +5 -0
- eoio-0.1.2.dist-info/licenses/LICENSE +165 -0
- eoio-0.1.2.dist-info/top_level.txt +1 -0
eoio/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""eoio - Data readers and preprocessing for satellite data"""
|
|
2
|
+
|
|
3
|
+
# import h5py # type: ignore
|
|
4
|
+
|
|
5
|
+
# from eoio._show_versions import show_versions
|
|
6
|
+
from eoio.interface import read, product_options # (
|
|
7
|
+
|
|
8
|
+
# product_bounds,
|
|
9
|
+
# product_processors,
|
|
10
|
+
# product_subsetting_params,
|
|
11
|
+
# read,
|
|
12
|
+
# write,
|
|
13
|
+
# )
|
|
14
|
+
|
|
15
|
+
# __all__ = ["read", "write" "show_versions"]
|
|
16
|
+
|
|
17
|
+
from ._version import get_versions
|
|
18
|
+
|
|
19
|
+
__version__ = get_versions()["version"]
|
|
20
|
+
del get_versions
|
eoio/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
# This file was generated by 'versioneer.py' (0.29) from
|
|
3
|
+
# revision-control system data, or from the parent directory name of an
|
|
4
|
+
# unpacked source archive. Distribution tarballs contain a pre-generated copy
|
|
5
|
+
# of this file.
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
version_json = '''
|
|
10
|
+
{
|
|
11
|
+
"date": "2026-04-12T15:50:31+0100",
|
|
12
|
+
"dirty": false,
|
|
13
|
+
"error": null,
|
|
14
|
+
"full-revisionid": "198b8c0b0998d0857c3687e2065feb303b389480",
|
|
15
|
+
"version": "0.1.2"
|
|
16
|
+
}
|
|
17
|
+
''' # END VERSION_JSON
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_versions():
|
|
21
|
+
return json.loads(version_json)
|
eoio/deps.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""eoio.deps - Lazy imports for optional dependencies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import pyproj # noqa: F401
|
|
8
|
+
import rasterio # noqa: F401
|
|
9
|
+
import rioxarray # noqa: F401
|
|
10
|
+
import cfgrib # noqa: F401
|
|
11
|
+
from osgeo import gdal, ogr, osr # noqa: F401
|
|
12
|
+
import shapely # noqa: F401
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _require_extra(extra: str, import_name: str) -> None:
|
|
16
|
+
raise ModuleNotFoundError(
|
|
17
|
+
f"Optional dependency '{import_name}' is required for this operation. "
|
|
18
|
+
f"Install with: pip install eoio[{extra}]"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def lazy_pyproj():
|
|
23
|
+
try:
|
|
24
|
+
import pyproj # type: ignore
|
|
25
|
+
except ModuleNotFoundError:
|
|
26
|
+
_require_extra("geo", "pyproj")
|
|
27
|
+
return pyproj
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def lazy_shapely():
|
|
31
|
+
try:
|
|
32
|
+
from shapely.geometry import Polygon, box, mapping, shape # type: ignore
|
|
33
|
+
from shapely.ops import transform as shp_transform # type: ignore
|
|
34
|
+
except ModuleNotFoundError:
|
|
35
|
+
_require_extra("geo", "shapely")
|
|
36
|
+
return Polygon, box, mapping, shape, shp_transform
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def lazy_rasterio():
|
|
40
|
+
try:
|
|
41
|
+
import rasterio # type: ignore
|
|
42
|
+
except ModuleNotFoundError:
|
|
43
|
+
_require_extra("raster", "rasterio")
|
|
44
|
+
return rasterio
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def lazy_rioxarray():
|
|
48
|
+
try:
|
|
49
|
+
import rioxarray as rxr # type: ignore
|
|
50
|
+
except ModuleNotFoundError:
|
|
51
|
+
_require_extra("raster", "rioxarray")
|
|
52
|
+
return rxr
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def lazy_cfgrib():
|
|
56
|
+
try:
|
|
57
|
+
import cfgrib # type: ignore
|
|
58
|
+
except ModuleNotFoundError:
|
|
59
|
+
raise ModuleNotFoundError(
|
|
60
|
+
f"Dependency 'cfgrib' is required for this operation."
|
|
61
|
+
f"Please see the 'cfgrib' instructions at: https://github.com/ecmwf/cfgrib?tab=readme-ov-file#installation"
|
|
62
|
+
)
|
|
63
|
+
return cfgrib
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
pass
|
eoio/interface.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""eoio.interface - interface functions module"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, List
|
|
4
|
+
import xarray as xr
|
|
5
|
+
import os
|
|
6
|
+
from processor_tools import Context
|
|
7
|
+
from eoio.processors.processor_pipeline import ProcessorPipeline
|
|
8
|
+
from eoio.processors.registry import PROCESSOR_REGISTRY
|
|
9
|
+
from eoio.readers.factory import ReaderFactory
|
|
10
|
+
from eoio.utils.read_utils import setup_file
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"read",
|
|
14
|
+
# "product_bounds",
|
|
15
|
+
"product_processors",
|
|
16
|
+
# "mid_lon_lat",
|
|
17
|
+
"product_options",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def read(
|
|
22
|
+
path: str,
|
|
23
|
+
vars_sel: Dict[str, List[str]] = None,
|
|
24
|
+
subset: Optional[Dict[str, Any]] = None,
|
|
25
|
+
read_params: Optional[Dict[str, Any]] = None,
|
|
26
|
+
processors: Optional[Dict[str, Any]] = None,
|
|
27
|
+
*args,
|
|
28
|
+
**kwargs,
|
|
29
|
+
) -> xr.Dataset:
|
|
30
|
+
"""
|
|
31
|
+
Reads an Earth Observation (EO) data product and returns the requested variables,
|
|
32
|
+
optionally applying spatial subsetting and post-processing.
|
|
33
|
+
|
|
34
|
+
:param path:
|
|
35
|
+
Path to the EO data product (file or directory, depending on product type).
|
|
36
|
+
|
|
37
|
+
:param vars_sel:
|
|
38
|
+
Variable selection dictionary defining which variables to read from the product.
|
|
39
|
+
|
|
40
|
+
Supported keys are:
|
|
41
|
+
|
|
42
|
+
* ``"meas"`` (*list[str] | str | None*, default: ``"all"``) –
|
|
43
|
+
Measurement variables to read. Options are:
|
|
44
|
+
|
|
45
|
+
- ``"all"`` – read all available measurement variables
|
|
46
|
+
- ``list[str]`` – explicit list of measurement variable names
|
|
47
|
+
- ``None`` – do not read any measurement variables
|
|
48
|
+
|
|
49
|
+
* ``"mask"`` (*list[str] | None*, default: ``None``) –
|
|
50
|
+
Mask variable names to read. Use ``None`` to disable mask reading.
|
|
51
|
+
|
|
52
|
+
* ``"aux"`` (*list[str] | None*, default: ``None``) –
|
|
53
|
+
Auxiliary data variable names to read. Use ``None`` to disable auxiliary data reading.
|
|
54
|
+
|
|
55
|
+
Available variable names and supported combinations for a given product
|
|
56
|
+
can be inspected via ``eoio.product_options``.
|
|
57
|
+
|
|
58
|
+
:param subset:
|
|
59
|
+
Optional definition of subsetting parameters. If omitted or ``None``,
|
|
60
|
+
the full data product is read without subsetting.
|
|
61
|
+
|
|
62
|
+
Supported keys (availability depends on the product type):
|
|
63
|
+
|
|
64
|
+
* ``"roi"`` –
|
|
65
|
+
Spatial region of interest for raster data. Supported forms are:
|
|
66
|
+
|
|
67
|
+
- ``None`` – no spatial subsetting
|
|
68
|
+
- ``shapely`` geometry (interpreted in ``roi_crs_epsg``)
|
|
69
|
+
- Bounding box tuple ``(xmin, ymin, xmax, ymax)`` in ``roi_crs_epsg``
|
|
70
|
+
- GeoJSON-like ``dict`` with a ``"type"`` key
|
|
71
|
+
- List of ``[x, y]`` coordinate pairs defining a polygon
|
|
72
|
+
- ``((x, y), half_width_m)`` defining a square region centred on a point
|
|
73
|
+
|
|
74
|
+
* ``"roi_crs_epsg"`` (*str*) –
|
|
75
|
+
EPSG code defining the coordinate reference system of ``roi``
|
|
76
|
+
(e.g. ``"EPSG:4326"``).
|
|
77
|
+
|
|
78
|
+
* ``"angle"`` – (*Not implemented*) Observation geometry range of interest
|
|
79
|
+
|
|
80
|
+
* ``"spectral"`` – (*Not implemented*) Spectral range of interest
|
|
81
|
+
|
|
82
|
+
:param read_params:
|
|
83
|
+
Optional parameters controlling how the data are read.
|
|
84
|
+
If omitted, default behaviour is used.
|
|
85
|
+
|
|
86
|
+
Supported options include:
|
|
87
|
+
|
|
88
|
+
* ``"save_extracted"`` (*bool*, default: ``False``) –
|
|
89
|
+
If the data product is extracted or uncompressed during reading,
|
|
90
|
+
controls whether the extracted files are saved to disk.
|
|
91
|
+
|
|
92
|
+
* ``"metadata_level"`` (*None | bool*, default: ``None``) –
|
|
93
|
+
Level of metadata to read:
|
|
94
|
+
|
|
95
|
+
- ``None`` – Standard/core metadata only
|
|
96
|
+
- ``False`` – Do not read metadata
|
|
97
|
+
- ``True`` – Read all available metadata
|
|
98
|
+
|
|
99
|
+
:param processors:
|
|
100
|
+
Optional definition of post-processing steps to apply after reading
|
|
101
|
+
(e.g. interpolation, unit conversion, etc.).
|
|
102
|
+
|
|
103
|
+
Dictionary keys should be the names of the processors to run.
|
|
104
|
+
The associated entry should be a subdictionary of parameters for that processor.
|
|
105
|
+
Required parameters are defined at the processor level.
|
|
106
|
+
|
|
107
|
+
:return:
|
|
108
|
+
``xarray.Dataset`` containing the requested variables and associated
|
|
109
|
+
metadata from the EO data product.
|
|
110
|
+
|
|
111
|
+
Examples
|
|
112
|
+
--------
|
|
113
|
+
.. doctest-skip::
|
|
114
|
+
|
|
115
|
+
Read all measurement variables from an EO product:
|
|
116
|
+
|
|
117
|
+
>>> ds = eoio.read("/path/to/product")
|
|
118
|
+
|
|
119
|
+
Read selected variables over a spatial region of interest:
|
|
120
|
+
|
|
121
|
+
>>> from shapely.geometry import box
|
|
122
|
+
>>> ds = read_product(
|
|
123
|
+
... path="/path/to/product",
|
|
124
|
+
... vars_sel={
|
|
125
|
+
... "meas": ["B02", "B03", "B04"],
|
|
126
|
+
... "mask": ["cloud_mask"],
|
|
127
|
+
... },
|
|
128
|
+
... subset={
|
|
129
|
+
... "roi": box(-2.0, 50.0, 0.0, 52.0),
|
|
130
|
+
... "roi_crs_epsg": "EPSG:4326",
|
|
131
|
+
... },
|
|
132
|
+
... )
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
if path is None:
|
|
136
|
+
raise TypeError("path must not be None")
|
|
137
|
+
|
|
138
|
+
if not os.path.exists(path):
|
|
139
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
140
|
+
|
|
141
|
+
# setup_file uncompresses file if necessary, and cleans up files after run
|
|
142
|
+
with setup_file(path, read_params) as path:
|
|
143
|
+
|
|
144
|
+
# Initialise reader
|
|
145
|
+
reader_factory = ReaderFactory()
|
|
146
|
+
reader_cls = reader_factory.get_reader(path)
|
|
147
|
+
reader_obj = reader_cls(
|
|
148
|
+
path, vars_sel=vars_sel, subset=subset, read_params=read_params
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Open dataset
|
|
152
|
+
ds = reader_obj.open()
|
|
153
|
+
|
|
154
|
+
# No post-processing requested
|
|
155
|
+
if not processors:
|
|
156
|
+
return ds
|
|
157
|
+
|
|
158
|
+
# Normalise context passed to processors
|
|
159
|
+
context = Context({**(subset or {}), "path": path})
|
|
160
|
+
|
|
161
|
+
# Run processor pipeline
|
|
162
|
+
pp = ProcessorPipeline(processor_params=processors, context=context)
|
|
163
|
+
|
|
164
|
+
ds = pp.run(ds)
|
|
165
|
+
|
|
166
|
+
return ds
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# def write(
|
|
170
|
+
# path_original: Union[str, List[str]],
|
|
171
|
+
# correction: Dict[str, Union[float, np.ndarray, int]],
|
|
172
|
+
# write_params: Optional[Dict[str, Any]] = None,
|
|
173
|
+
# ) -> None:
|
|
174
|
+
# """
|
|
175
|
+
# writer function
|
|
176
|
+
#
|
|
177
|
+
# :param path_original: satellite data product
|
|
178
|
+
# :param correction: dictionary with band names to be corrected as keys, and either corrected data or bias correction per band as values
|
|
179
|
+
# :param write_params: definition of desired writing parameters, by default None
|
|
180
|
+
# """
|
|
181
|
+
# if isinstance(path_original, str) or len(list(path_original)) == 1:
|
|
182
|
+
# path_original = [path_original]
|
|
183
|
+
#
|
|
184
|
+
# for fn in path_original:
|
|
185
|
+
# with setup_file(fn, write_params) as path:
|
|
186
|
+
# writer_factory = WriterFactory()
|
|
187
|
+
#
|
|
188
|
+
# writer = writer_factory.get_writer(path)
|
|
189
|
+
#
|
|
190
|
+
# writer_obj = writer()
|
|
191
|
+
#
|
|
192
|
+
# writer_obj.write(path, correction, write_params)
|
|
193
|
+
#
|
|
194
|
+
#
|
|
195
|
+
# def product_bounds(
|
|
196
|
+
# path: str,
|
|
197
|
+
# read_params: Optional[Dict[str, Any]] = None,
|
|
198
|
+
# *args,
|
|
199
|
+
# **kwargs,
|
|
200
|
+
# ) -> dict:
|
|
201
|
+
# """
|
|
202
|
+
# Return coordinate bounds of the product.
|
|
203
|
+
#
|
|
204
|
+
# Example output:
|
|
205
|
+
# {
|
|
206
|
+
# 'EPSG:4326':
|
|
207
|
+
# [
|
|
208
|
+
# [108.60281738078888, 41.52685230104365],
|
|
209
|
+
# [109.91862807140421, 41.54675989693983],
|
|
210
|
+
# [109.93470404984265, 40.55774811423141],
|
|
211
|
+
# [108.63842218519015, 40.5385178383516]],
|
|
212
|
+
# 'EPSG:32649':
|
|
213
|
+
# [
|
|
214
|
+
# [300000.0, 4600020.0],
|
|
215
|
+
# [409810.0, 4600020.0],
|
|
216
|
+
# [409810.0, 4490210.0],
|
|
217
|
+
# [300000.0, 4490210.0]
|
|
218
|
+
# ]
|
|
219
|
+
# }
|
|
220
|
+
#
|
|
221
|
+
# :param path: satellite data product
|
|
222
|
+
# :return: dictionary with coordinate reference system as a key and the corresponding coordinate bounds as values
|
|
223
|
+
# """
|
|
224
|
+
# with setup_file(path, read_params) as path:
|
|
225
|
+
# reader_factory = ReaderFactory()
|
|
226
|
+
#
|
|
227
|
+
# reader = reader_factory.get_reader(path)
|
|
228
|
+
#
|
|
229
|
+
# reader_obj = reader(path)
|
|
230
|
+
#
|
|
231
|
+
# try:
|
|
232
|
+
# return dict(
|
|
233
|
+
# [(k, list(v.exterior.coords)) for k, v in reader_obj.bounds.items()]
|
|
234
|
+
# )
|
|
235
|
+
# except AttributeError:
|
|
236
|
+
# raise ValueError(
|
|
237
|
+
# """'product_bounds' cannot be determined from '{}'.
|
|
238
|
+
# Either the bounds of the product cannot be parsed without reading in
|
|
239
|
+
# the full product or the reader is not yet fully configured.""".format(
|
|
240
|
+
# path
|
|
241
|
+
# )
|
|
242
|
+
# )
|
|
243
|
+
#
|
|
244
|
+
#
|
|
245
|
+
# def mid_lon_lat(
|
|
246
|
+
# path: str,
|
|
247
|
+
# *args,
|
|
248
|
+
# **kwargs,
|
|
249
|
+
# ) -> Tuple[float, float]:
|
|
250
|
+
# """
|
|
251
|
+
# Return mid point of satellite product in as a (lon, lat) coordinate
|
|
252
|
+
#
|
|
253
|
+
# :param path: satellite data product
|
|
254
|
+
# :return : tuple of the (lon, lat) coordinate for the centre of the satellite product
|
|
255
|
+
# """
|
|
256
|
+
# bounds = product_bounds(path, *args, **kwargs)["EPSG:4326"]
|
|
257
|
+
#
|
|
258
|
+
# lons, lats = [*set([i[0] for i in bounds])], [*set([i[1] for i in bounds])]
|
|
259
|
+
# lon_0, lon_1, lat_0, lat_1 = min(lons), max(lons), min(lats), max(lats)
|
|
260
|
+
# return lon_0 + (lon_1 - lon_0) / 2, lat_0 + (lat_1 - lat_0) / 2
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def product_processors(path: str, *args, **kwargs) -> Optional[dict]:
|
|
264
|
+
"""
|
|
265
|
+
Return dictionary of available post processors for the requested satellite data product
|
|
266
|
+
and their optional parameters. Return None if none available or the product is not recognised.
|
|
267
|
+
|
|
268
|
+
:param path: satellite data product
|
|
269
|
+
:return : dictionary of available post processors and their optional parameters
|
|
270
|
+
"""
|
|
271
|
+
from eoio.processors.registry import PROCESSOR_REGISTRY
|
|
272
|
+
|
|
273
|
+
processor_info = {}
|
|
274
|
+
for processor_name in PROCESSOR_REGISTRY:
|
|
275
|
+
processor_info[processor_name] = PROCESSOR_REGISTRY[processor_name]._all_options
|
|
276
|
+
return processor_info
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def product_options(
|
|
280
|
+
path: str, read_params: Optional[Dict[str, Any]] = None, *args, **kwargs
|
|
281
|
+
) -> dict:
|
|
282
|
+
"""
|
|
283
|
+
Return dictionary of available `meas_vars` options for the requested EO
|
|
284
|
+
data product
|
|
285
|
+
|
|
286
|
+
:param path: satellite data product
|
|
287
|
+
:return : dictionary of available subsetting parameters
|
|
288
|
+
"""
|
|
289
|
+
with setup_file(path, read_params) as path:
|
|
290
|
+
reader_factory = ReaderFactory()
|
|
291
|
+
|
|
292
|
+
reader = reader_factory.get_reader(path)
|
|
293
|
+
|
|
294
|
+
reader_obj = reader(path)
|
|
295
|
+
|
|
296
|
+
return reader_obj.all_options
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
pass
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
eoio.processors.add_lat_lon.processor
|
|
3
|
+
-------------------------------
|
|
4
|
+
|
|
5
|
+
Top-level latitude/longitude processor.
|
|
6
|
+
|
|
7
|
+
This processor provides a single, stable user-facing interface (``add_lat_lon``).
|
|
8
|
+
|
|
9
|
+
User config example
|
|
10
|
+
-------------------
|
|
11
|
+
processors= {
|
|
12
|
+
"add_lat_lon": {
|
|
13
|
+
"geometry_id": ["10m", "60m"],
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any, Dict, Mapping, Optional, Sequence
|
|
22
|
+
from processor_tools import BaseProcessor
|
|
23
|
+
import xarray as xr
|
|
24
|
+
from eoio.processors.registry import register_processor
|
|
25
|
+
from eoio.deps import lazy_pyproj
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class AddLatLonConfig:
|
|
31
|
+
"""
|
|
32
|
+
Parameters for the add_lat_lon processor.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
geometry_id: Optional[Sequence[str]] = None # e.g. ["10m", "60m"]
|
|
36
|
+
on_missing: str = "error" # "error" | "skip"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@register_processor("add_lat_lon")
|
|
40
|
+
class AddLatLon(BaseProcessor):
|
|
41
|
+
"""
|
|
42
|
+
Add latitude and longitude coordinates to a dataset.
|
|
43
|
+
|
|
44
|
+
Processor parameters
|
|
45
|
+
--------------------
|
|
46
|
+
|
|
47
|
+
The following parameters can be provided in the `params` dict:
|
|
48
|
+
|
|
49
|
+
:param geometry_id:
|
|
50
|
+
Optional, List of geometry IDs to add (e.g. ``["10m", "60m"]``), if omitted lat/lon will be added for all grid resolutions found in the dataset.
|
|
51
|
+
:param on_missing:
|
|
52
|
+
Behaviour if required metadata for conversion is missing.
|
|
53
|
+
Supported values are ``"error"`` (default, if omitted) or ``"skip"``.
|
|
54
|
+
|
|
55
|
+
Notes
|
|
56
|
+
-----
|
|
57
|
+
- This processor is intended to run after reading.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
_all_options = {
|
|
61
|
+
"geometry_id": "Optional, List of geometry IDs to add (e.g. ['10m', '60m']), if omitted lat/lon will be added for all grid resolutions found in the dataset.",
|
|
62
|
+
"on_missing": "Behaviour if required metadata for conversion is missing. Supported values are 'error' (default, if omitted) or 'skip'.",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
params: Optional[Dict[str, Any]] = None,
|
|
68
|
+
context: Optional[Dict[str, Any]] = None,
|
|
69
|
+
):
|
|
70
|
+
"""
|
|
71
|
+
Create an add_lat_lon processor.
|
|
72
|
+
|
|
73
|
+
:param params:
|
|
74
|
+
Processor parameters (see class docstring for details)
|
|
75
|
+
:param context:
|
|
76
|
+
Processing context provided by eoio (reader info, metadata view, logger, etc.).
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
super().__init__(context=context)
|
|
80
|
+
self.add_lat_lon_config = self._parse_params(params or {})
|
|
81
|
+
|
|
82
|
+
def _parse_params(self, params: Dict[str, Any]) -> AddLatLonConfig:
|
|
83
|
+
"""
|
|
84
|
+
Validate and normalise processor parameters.
|
|
85
|
+
|
|
86
|
+
:param params:
|
|
87
|
+
Raw params dict from the processor spec.
|
|
88
|
+
:return:
|
|
89
|
+
Parsed AddLatLonParams.
|
|
90
|
+
:raises ValueError:
|
|
91
|
+
If required parameters are missing or invalid.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
# resolve "geometry_id" param
|
|
95
|
+
geometry_id = params.get("geometry_id", None)
|
|
96
|
+
|
|
97
|
+
# resolve "on_missing" param
|
|
98
|
+
on_missing = str(params.get("on_missing", "error")).lower()
|
|
99
|
+
if on_missing not in {"error", "skip"}:
|
|
100
|
+
raise ValueError("interpolate: 'on_missing' must be 'error' or 'skip'.")
|
|
101
|
+
|
|
102
|
+
return AddLatLonConfig(
|
|
103
|
+
geometry_id=geometry_id,
|
|
104
|
+
on_missing=on_missing,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _format_geometry_id(self, ds: xr.Dataset, geometry_id: Sequence) -> Sequence:
|
|
108
|
+
"""
|
|
109
|
+
Format the geometry ids to ensure they are in the correct format for lat/lon processing.
|
|
110
|
+
|
|
111
|
+
:param ds:
|
|
112
|
+
Input dataset (used for context, e.g. to check available coordinates).
|
|
113
|
+
:param geometry_id:
|
|
114
|
+
List of geometry ids to process lat/lon for (e.g. ``["10m", "60m"]``).
|
|
115
|
+
"""
|
|
116
|
+
# available_geoms = list(set(ds.geometry_id))
|
|
117
|
+
available_geoms = list(
|
|
118
|
+
set([x.split("_")[-1] for x in ds.coords if "x_" in x or "y_" in x])
|
|
119
|
+
)
|
|
120
|
+
# Check all coords exist in the dataset
|
|
121
|
+
if geometry_id is not None:
|
|
122
|
+
for geom in geometry_id:
|
|
123
|
+
if geom not in available_geoms:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"AddLatLon: geometry_id '{geom}' not found in dataset."
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
geometry_id = available_geoms
|
|
129
|
+
|
|
130
|
+
return geometry_id
|
|
131
|
+
|
|
132
|
+
def run(self, ds: xr.Dataset) -> xr.Dataset:
|
|
133
|
+
"""
|
|
134
|
+
Run add lat/lon on the dataset.
|
|
135
|
+
|
|
136
|
+
:param ds:
|
|
137
|
+
Input dataset.
|
|
138
|
+
:return:
|
|
139
|
+
Output dataset with lat/lon coords.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
if not isinstance(ds, xr.Dataset):
|
|
143
|
+
raise TypeError("add_lat_lon: input must be an xarray.Dataset.")
|
|
144
|
+
|
|
145
|
+
context: Mapping[str, Any] = self.context or {}
|
|
146
|
+
|
|
147
|
+
# get geometry ids
|
|
148
|
+
geometry_id = self._format_geometry_id(ds, self.add_lat_lon_config.geometry_id)
|
|
149
|
+
|
|
150
|
+
# instantiate transformer
|
|
151
|
+
pyproj = lazy_pyproj()
|
|
152
|
+
crs_src = ds.rio.crs
|
|
153
|
+
crs_dst = pyproj.CRS.from_epsg(4326)
|
|
154
|
+
transformer = pyproj.Transformer.from_crs(crs_src, crs_dst, always_xy=True)
|
|
155
|
+
|
|
156
|
+
# add lat/lon as coords
|
|
157
|
+
for geom in geometry_id:
|
|
158
|
+
x, y = np.meshgrid(ds[f"x_{geom}"].values, ds[f"y_{geom}"].values)
|
|
159
|
+
lons, lats = transformer.transform(x, y)
|
|
160
|
+
|
|
161
|
+
lat_lon_dict = {
|
|
162
|
+
f"latitude_{geom}": ([f"y_{geom}", f"x_{geom}"], lats),
|
|
163
|
+
f"longitude_{geom}": ([f"y_{geom}", f"x_{geom}"], lons),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
ds = ds.assign_coords(coords=lat_lon_dict)
|
|
167
|
+
|
|
168
|
+
# Record processing history
|
|
169
|
+
ds = self._record_provenance(ds)
|
|
170
|
+
|
|
171
|
+
return ds
|
|
172
|
+
|
|
173
|
+
def _record_provenance(
|
|
174
|
+
self,
|
|
175
|
+
ds: xr.Dataset,
|
|
176
|
+
) -> xr.Dataset:
|
|
177
|
+
"""
|
|
178
|
+
Record a minimal provenance entry at processor level.
|
|
179
|
+
|
|
180
|
+
:param ds:
|
|
181
|
+
Output dataset.
|
|
182
|
+
:return:
|
|
183
|
+
Dataset (same object, attrs updated).
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
steps: list = ds.attrs.get("eoio:processing_steps", [])
|
|
187
|
+
if not isinstance(steps, list):
|
|
188
|
+
steps = [str(steps)]
|
|
189
|
+
|
|
190
|
+
steps.append(
|
|
191
|
+
{
|
|
192
|
+
"processor": "add_lat_lon",
|
|
193
|
+
"geometry_ids": self._format_geometry_id(
|
|
194
|
+
ds, self.add_lat_lon_config.geometry_id
|
|
195
|
+
),
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
ds.attrs["eoio:processing_steps"] = steps
|
|
199
|
+
return ds
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
if __name__ == "__main__":
|
|
203
|
+
pass
|
|
File without changes
|