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.
Files changed (227) hide show
  1. eoio/__init__.py +20 -0
  2. eoio/_version.py +21 -0
  3. eoio/deps.py +67 -0
  4. eoio/interface.py +300 -0
  5. eoio/processors/__init__.py +4 -0
  6. eoio/processors/add_lat_lon/__init__.py +4 -0
  7. eoio/processors/add_lat_lon/processor.py +203 -0
  8. eoio/processors/add_lat_lon/tests/__init__.py +0 -0
  9. eoio/processors/add_lat_lon/tests/test_processor.py +168 -0
  10. eoio/processors/interpolate/__init__.py +4 -0
  11. eoio/processors/interpolate/processor.py +454 -0
  12. eoio/processors/interpolate/tests/__init__.py +0 -0
  13. eoio/processors/interpolate/tests/test_processor.py +168 -0
  14. eoio/processors/processor_pipeline.py +147 -0
  15. eoio/processors/registry.py +34 -0
  16. eoio/processors/s2_rut/__init__.py +4 -0
  17. eoio/processors/s2_rut/processor.py +176 -0
  18. eoio/processors/s2_rut/tests/__init__.py +0 -0
  19. eoio/processors/s2_rut/tests/test_processor.py +131 -0
  20. eoio/processors/tests/__init__.py +0 -0
  21. eoio/processors/tests/test_processor_pipeline.py +260 -0
  22. eoio/processors/units/__init__.py +4 -0
  23. eoio/processors/units/drivers/__init__.py +4 -0
  24. eoio/processors/units/drivers/base.py +97 -0
  25. eoio/processors/units/drivers/registry.py +34 -0
  26. eoio/processors/units/drivers/s2lmsi1c.py +99 -0
  27. eoio/processors/units/drivers/tests/__init__.py +0 -0
  28. eoio/processors/units/processor.py +273 -0
  29. eoio/processors/units/tests/__init__.py +0 -0
  30. eoio/readers/__init__.py +18 -0
  31. eoio/readers/airbus_pleiades/__init__.py +0 -0
  32. eoio/readers/airbus_pleiades/aux_data.py +181 -0
  33. eoio/readers/airbus_pleiades/conventions.py +36 -0
  34. eoio/readers/airbus_pleiades/data_io.py +146 -0
  35. eoio/readers/airbus_pleiades/layout.py +86 -0
  36. eoio/readers/airbus_pleiades/metadata.py +359 -0
  37. eoio/readers/airbus_pleiades/reader.py +166 -0
  38. eoio/readers/airbus_pleiades/tests/__init__.py +0 -0
  39. eoio/readers/airbus_pleiades/tests/test_aux_data.py +228 -0
  40. eoio/readers/airbus_pleiades/tests/test_conventions.py +34 -0
  41. eoio/readers/airbus_pleiades/tests/test_data_io.py +256 -0
  42. eoio/readers/airbus_pleiades/tests/test_layout.py +188 -0
  43. eoio/readers/airbus_pleiades/tests/test_metadata.py +352 -0
  44. eoio/readers/airbus_pleiades/tests/test_reader.py +223 -0
  45. eoio/readers/base.py +358 -0
  46. eoio/readers/emit/__init__.py +0 -0
  47. eoio/readers/emit/angles.py +59 -0
  48. eoio/readers/emit/aux_data.py +143 -0
  49. eoio/readers/emit/data_io.py +131 -0
  50. eoio/readers/emit/layout.py +117 -0
  51. eoio/readers/emit/metadata.py +142 -0
  52. eoio/readers/emit/reader.py +149 -0
  53. eoio/readers/emit/subset.py +82 -0
  54. eoio/readers/emit/tests/__init__.py +0 -0
  55. eoio/readers/emit/tests/test_angles.py +44 -0
  56. eoio/readers/emit/tests/test_aux_data.py +38 -0
  57. eoio/readers/emit/tests/test_data_io.py +26 -0
  58. eoio/readers/emit/tests/test_layout.py +64 -0
  59. eoio/readers/emit/tests/test_metadata.py +86 -0
  60. eoio/readers/emit/tests/test_reader.py +43 -0
  61. eoio/readers/emit/tests/test_subset.py +51 -0
  62. eoio/readers/era5/reader.py +114 -0
  63. eoio/readers/era5/subset.py +33 -0
  64. eoio/readers/factory.py +131 -0
  65. eoio/readers/generic_netcdf/__init__.py +0 -0
  66. eoio/readers/generic_netcdf/data_io.py +71 -0
  67. eoio/readers/generic_netcdf/metadata.py +126 -0
  68. eoio/readers/generic_netcdf/reader.py +94 -0
  69. eoio/readers/generic_netcdf/subset.py +83 -0
  70. eoio/readers/generic_netcdf/tests/__init__.py +0 -0
  71. eoio/readers/generic_netcdf/tests/test_data_io.py +33 -0
  72. eoio/readers/generic_netcdf/tests/test_metadata.py +30 -0
  73. eoio/readers/generic_netcdf/tests/test_reader.py +34 -0
  74. eoio/readers/generic_netcdf/tests/test_subset.py +28 -0
  75. eoio/readers/hypernets/__init__.py +21 -0
  76. eoio/readers/hypernets/data_io.py +54 -0
  77. eoio/readers/hypernets/metadata.py +129 -0
  78. eoio/readers/hypernets/reader.py +324 -0
  79. eoio/readers/hypernets/subset.py +152 -0
  80. eoio/readers/hypernets/tests/__init__.py +0 -0
  81. eoio/readers/hypernets/tests/test_data_io.py +43 -0
  82. eoio/readers/hypernets/tests/test_metadata.py +50 -0
  83. eoio/readers/hypernets/tests/test_reader.py +195 -0
  84. eoio/readers/hypernets/tests/test_reader_end_to_end.py +204 -0
  85. eoio/readers/hypernets/tests/test_subset.py +130 -0
  86. eoio/readers/landsat/__init__.py +1 -0
  87. eoio/readers/landsat/aux_data/__init__.py +1 -0
  88. eoio/readers/landsat/aux_data/angles.py +97 -0
  89. eoio/readers/landsat/aux_data/aux_data.py +70 -0
  90. eoio/readers/landsat/aux_data/tests/__init__.py +0 -0
  91. eoio/readers/landsat/aux_data/tests/test_angles.py +105 -0
  92. eoio/readers/landsat/aux_data/tests/test_aux.py +56 -0
  93. eoio/readers/landsat/conventions.py +39 -0
  94. eoio/readers/landsat/data_io.py +157 -0
  95. eoio/readers/landsat/layout.py +156 -0
  96. eoio/readers/landsat/metadata/__init__.py +54 -0
  97. eoio/readers/landsat/metadata/extractor.py +372 -0
  98. eoio/readers/landsat/metadata/ls_mtd_json.py +176 -0
  99. eoio/readers/landsat/metadata/ls_mtd_xml.py +328 -0
  100. eoio/readers/landsat/metadata/tests/__init__.py +1 -0
  101. eoio/readers/landsat/metadata/tests/test_extractor.py +159 -0
  102. eoio/readers/landsat/metadata/tests/test_ls_mtd_json.py +114 -0
  103. eoio/readers/landsat/metadata/tests/test_ls_mtd_xml.py +138 -0
  104. eoio/readers/landsat/reader.py +210 -0
  105. eoio/readers/landsat/tests/__init__.py +0 -0
  106. eoio/readers/landsat/tests/test_conventions.py +33 -0
  107. eoio/readers/landsat/tests/test_data_io.py +123 -0
  108. eoio/readers/landsat/tests/test_layout.py +125 -0
  109. eoio/readers/landsat/tests/test_reader.py +91 -0
  110. eoio/readers/metadata.py +201 -0
  111. eoio/readers/planetscope/__init__.py +1 -0
  112. eoio/readers/planetscope/aux_data.py +103 -0
  113. eoio/readers/planetscope/conventions.py +37 -0
  114. eoio/readers/planetscope/data_io.py +143 -0
  115. eoio/readers/planetscope/layout.py +93 -0
  116. eoio/readers/planetscope/metadata.py +336 -0
  117. eoio/readers/planetscope/reader.py +193 -0
  118. eoio/readers/planetscope/tests/__init__.py +0 -0
  119. eoio/readers/planetscope/tests/test_aux_data.py +155 -0
  120. eoio/readers/planetscope/tests/test_conventions.py +36 -0
  121. eoio/readers/planetscope/tests/test_data_io.py +218 -0
  122. eoio/readers/planetscope/tests/test_layout.py +143 -0
  123. eoio/readers/planetscope/tests/test_metadata.py +296 -0
  124. eoio/readers/planetscope/tests/test_reader.py +230 -0
  125. eoio/readers/radcalnet/__init__.py +0 -0
  126. eoio/readers/radcalnet/data_io.py +98 -0
  127. eoio/readers/radcalnet/metadata.py +134 -0
  128. eoio/readers/radcalnet/reader.py +190 -0
  129. eoio/readers/radcalnet/subset.py +144 -0
  130. eoio/readers/radcalnet/tests/__init__.py +0 -0
  131. eoio/readers/radcalnet/tests/test_data_io.py +48 -0
  132. eoio/readers/radcalnet/tests/test_reader.py +382 -0
  133. eoio/readers/radcalnet/tests/test_subset.py +180 -0
  134. eoio/readers/radcalnet/utils.py +31 -0
  135. eoio/readers/sentinel2/__init__.py +0 -0
  136. eoio/readers/sentinel2/aux_vars/__init__.py +0 -0
  137. eoio/readers/sentinel2/aux_vars/angles.py +101 -0
  138. eoio/readers/sentinel2/aux_vars/aux_data.py +79 -0
  139. eoio/readers/sentinel2/aux_vars/masks.py +18 -0
  140. eoio/readers/sentinel2/aux_vars/meteo.py +83 -0
  141. eoio/readers/sentinel2/aux_vars/tests/__init__.py +0 -0
  142. eoio/readers/sentinel2/aux_vars/tests/test_angles.py +124 -0
  143. eoio/readers/sentinel2/aux_vars/tests/test_aux.py +216 -0
  144. eoio/readers/sentinel2/aux_vars/tests/test_masks.py +30 -0
  145. eoio/readers/sentinel2/aux_vars/tests/test_meteo.py +317 -0
  146. eoio/readers/sentinel2/conventions.py +26 -0
  147. eoio/readers/sentinel2/data_io.py +232 -0
  148. eoio/readers/sentinel2/layout.py +680 -0
  149. eoio/readers/sentinel2/metadata/__init__.py +0 -0
  150. eoio/readers/sentinel2/metadata/extractor.py +282 -0
  151. eoio/readers/sentinel2/metadata/s2_ds_mtd.py +85 -0
  152. eoio/readers/sentinel2/metadata/s2_prod_mtd.py +368 -0
  153. eoio/readers/sentinel2/metadata/s2_tl_mtd.py +386 -0
  154. eoio/readers/sentinel2/metadata/tests/__init__.py +0 -0
  155. eoio/readers/sentinel2/metadata/tests/test_extractor.py +473 -0
  156. eoio/readers/sentinel2/metadata/tests/test_s2_ds_mtd.py +133 -0
  157. eoio/readers/sentinel2/metadata/tests/test_s2_prod_mtd.py +206 -0
  158. eoio/readers/sentinel2/metadata/tests/test_s2_tl_mtd.py +299 -0
  159. eoio/readers/sentinel2/metadata/var_names.py +104 -0
  160. eoio/readers/sentinel2/reader.py +156 -0
  161. eoio/readers/sentinel2/tests/__init__.py +0 -0
  162. eoio/readers/sentinel2/tests/test_conventions.py +55 -0
  163. eoio/readers/sentinel2/tests/test_data_io.py +472 -0
  164. eoio/readers/sentinel2/tests/test_layout.py +325 -0
  165. eoio/readers/sentinel2/tests/test_layout_l2a.py +69 -0
  166. eoio/readers/sentinel2/tests/test_reader.py +236 -0
  167. eoio/readers/sentinel3_olci/__init__.py +5 -0
  168. eoio/readers/sentinel3_olci/auxiliary.py +651 -0
  169. eoio/readers/sentinel3_olci/conventions.py +6 -0
  170. eoio/readers/sentinel3_olci/data_io.py +349 -0
  171. eoio/readers/sentinel3_olci/layout.py +229 -0
  172. eoio/readers/sentinel3_olci/masks.py +146 -0
  173. eoio/readers/sentinel3_olci/metadata/extractor.py +263 -0
  174. eoio/readers/sentinel3_olci/metadata/s3_olci_mtd.py +373 -0
  175. eoio/readers/sentinel3_olci/metadata/tests/__init__.py +1 -0
  176. eoio/readers/sentinel3_olci/metadata/tests/test_extractor.py +335 -0
  177. eoio/readers/sentinel3_olci/metadata/tests/test_s3_olci_mtd.py +308 -0
  178. eoio/readers/sentinel3_olci/metadata/var_names.py +33 -0
  179. eoio/readers/sentinel3_olci/reader.py +260 -0
  180. eoio/readers/sentinel3_olci/tests/__init__.py +0 -0
  181. eoio/readers/sentinel3_olci/tests/test_auxiliary.py +133 -0
  182. eoio/readers/sentinel3_olci/tests/test_conventions.py +17 -0
  183. eoio/readers/sentinel3_olci/tests/test_data_io.py +254 -0
  184. eoio/readers/sentinel3_olci/tests/test_layout.py +205 -0
  185. eoio/readers/sentinel3_olci/tests/test_masks.py +195 -0
  186. eoio/readers/sentinel3_olci/tests/test_reader.py +273 -0
  187. eoio/readers/subset/__init__.py +0 -0
  188. eoio/readers/subset/angle_subset.py +180 -0
  189. eoio/readers/subset/base_subset.py +349 -0
  190. eoio/readers/subset/datetime_subset.py +146 -0
  191. eoio/readers/subset/roi_subset.py +460 -0
  192. eoio/readers/subset/tests/__init__.py +0 -0
  193. eoio/readers/subset/tests/test_roi_subset.py +148 -0
  194. eoio/readers/subset/tests/test_wavelength_subset.py +161 -0
  195. eoio/readers/subset/time_of_day_subset.py +163 -0
  196. eoio/readers/subset/wavelength_subset.py +53 -0
  197. eoio/readers/tests/__init__.py +0 -0
  198. eoio/readers/tests/test_base.py +216 -0
  199. eoio/readers/tests/test_factory.py +83 -0
  200. eoio/readers/tests/test_metadata.py +212 -0
  201. eoio/readers/tests/test_xml.py +398 -0
  202. eoio/readers/xml.py +407 -0
  203. eoio/tests/__init__.py +0 -0
  204. eoio/tests/test_deps.py +58 -0
  205. eoio/tests/test_interface.py +76 -0
  206. eoio/utils/__init__.py +0 -0
  207. eoio/utils/crs_utils.py +199 -0
  208. eoio/utils/dict_tools.py +751 -0
  209. eoio/utils/formatters.py +171 -0
  210. eoio/utils/instrument_rsr.py +95 -0
  211. eoio/utils/jp2_tools.py +86 -0
  212. eoio/utils/rasterio_utils.py +82 -0
  213. eoio/utils/read_utils.py +289 -0
  214. eoio/utils/tests/__init__.py +0 -0
  215. eoio/utils/tests/test_crs_utils.py +168 -0
  216. eoio/utils/tests/test_dict_tools.py +932 -0
  217. eoio/utils/tests/test_formatters.py +134 -0
  218. eoio/utils/tests/test_instrument_rsr.py +52 -0
  219. eoio/utils/tests/test_jp2_tools.py +98 -0
  220. eoio/utils/tests/test_read_utils.py +533 -0
  221. eoio/utils/tests/test_tif_tools.py +140 -0
  222. eoio/utils/tif_tools.py +78 -0
  223. eoio-0.1.2.dist-info/METADATA +99 -0
  224. eoio-0.1.2.dist-info/RECORD +227 -0
  225. eoio-0.1.2.dist-info/WHEEL +5 -0
  226. eoio-0.1.2.dist-info/licenses/LICENSE +165 -0
  227. 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,4 @@
1
+ # from . import units
2
+ from . import interpolate
3
+ from . import add_lat_lon
4
+ from . import s2_rut
@@ -0,0 +1,4 @@
1
+ import pkgutil, importlib
2
+
3
+ for m in pkgutil.walk_packages(__path__, prefix=__name__ + "."):
4
+ importlib.import_module(m.name)
@@ -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