From 5ed4a6b38bcb2a59ddb8d8dead6cde7d8b648a17 Mon Sep 17 00:00:00 2001 From: Victor Phan Date: Sun, 2 Nov 2025 23:36:32 +0700 Subject: [PATCH] =?UTF-8?q?Load=20data=20nh=C6=B0ng=20ch=E1=BB=89=20c?= =?UTF-8?q?=C3=B3=200.02MB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 5 + __pycache__/new_import_ODC.cpython-310.pyc | Bin 0 -> 14272 bytes __pycache__/new_import_ODC.cpython-313.pyc | Bin 0 -> 20059 bytes __pycache__/utils.cpython-310.pyc | Bin 0 -> 320 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 681 -> 678 bytes .../__pycache__/bandindices.cpython-310.pyc | Bin 19153 -> 19150 bytes easi_tools/__init__.py | 9 + .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 354 bytes .../__pycache__/deployments.cpython-310.pyc | Bin 0 -> 10758 bytes .../__pycache__/load_s2l2a.cpython-310.pyc | Bin 0 -> 6624 bytes .../notebook_utils.cpython-310.pyc | Bin 0 -> 5152 bytes easi_tools/deployments.py | 335 ++++++++++++++++++ easi_tools/load_s2l2a.py | 293 +++++++++++++++ easi_tools/notebook_utils.py | 171 +++++++++ new_import_ODC.py | 145 ++++++-- 15 files changed, 919 insertions(+), 39 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 __pycache__/new_import_ODC.cpython-310.pyc create mode 100644 __pycache__/new_import_ODC.cpython-313.pyc create mode 100644 __pycache__/utils.cpython-310.pyc create mode 100644 easi_tools/__init__.py create mode 100644 easi_tools/__pycache__/__init__.cpython-310.pyc create mode 100644 easi_tools/__pycache__/deployments.cpython-310.pyc create mode 100644 easi_tools/__pycache__/load_s2l2a.cpython-310.pyc create mode 100644 easi_tools/__pycache__/notebook_utils.cpython-310.pyc create mode 100644 easi_tools/deployments.py create mode 100644 easi_tools/load_s2l2a.py create mode 100644 easi_tools/notebook_utils.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a8c2003 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:conda", + "python-envs.defaultPackageManager": "ms-python.python:conda", + "python-envs.pythonProjects": [] +} \ No newline at end of file diff --git a/__pycache__/new_import_ODC.cpython-310.pyc b/__pycache__/new_import_ODC.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6503c27a8c3d938095c682ce6a4554306d87f13b GIT binary patch literal 14272 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 literal 0 HcmV?d00001 diff --git a/__pycache__/utils.cpython-310.pyc b/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c973edf87d5be437bc2f393919d8ebccf9ad670 GIT binary patch literal 320 zcmYjMO-lnY5Y3l%TUZf~{(>&3dlC^u#GbrVvEZSX5}fRAAiG(T6cp;sAEL+prCdGv z7d$yxdgz0BGn2=>nb5pq1qC6 zi1puD+1OTN>Ze9}b$R}xRp?|}u7fuIqJNLMvFHUd$W^VZNV^kF rsGu&xq7ZS6LN7az-lh8@_rVR?uy2n>NzHCr3u~ip)87WKjGXX)YK}rZ literal 0 HcmV?d00001 diff --git a/deafrica_tools/__pycache__/__init__.cpython-310.pyc b/deafrica_tools/__pycache__/__init__.cpython-310.pyc index 068b02d5dd66f1da3e88729228063b220c3b45b8..6328f148cb8e22657191a09ba46b5b44b660f27f 100644 GIT binary patch delta 124 zcmZ3c diff --git a/deafrica_tools/__pycache__/bandindices.cpython-310.pyc b/deafrica_tools/__pycache__/bandindices.cpython-310.pyc index e0c5b540f03f84d0e59988f91f1bd5c8254519b2..288e1d59d835d1973774e46c041a15602e752f03 100644 GIT binary patch delta 1708 zcmZvcT}+!*7{_`0@v_n~>!w1H(w0)%myeaPLSc?M3KTlmg0#Z+vaKu7OdKt6fMhf3 z#Lb!LHa8#l;ilV|IK{*$!DwQl7bf1QG0|i(_QL4ZuBO?QH@bNK=N$zBr7xh*IBrp8vT7^l33az-942wD*gL@Tyn2$k{LzUdL*Ktht zK`HLAe9XmZ9mhijDiu4o8Qff^3cH&oon}poehRsINfpEsLB;K zYtgudndldXi_W`JC7bwGF|rb9h1~?nE$sfvm#jjEwz4&bTa};vzu^aWs}vwhRh!g= zf=xGZu$co*=a!fAmY?&M`<*tDJvN>&a%%T@%t6=+ff}0fxD2fbz}<8#NK2kZZLdwy zB8%L7qP_lVRYq4>mFW9N)ow-ACprd!ZXl%UJEHxnJ`6;9`!TLWB&UzA4n)ViK|O4Yr)VFyHogS+j8 z+)icn?<>0@?t$9CEPPQ7UA&l_J{}LN>o~N?65B_&>%VS@!cqRuun|K`33j~*l+#7S zD&5+&Se<}b1@;36IQV5xKu!TkV2D2Phoxcq%YVW8C=_)-8?b$Op`lP8^FUsGW^0}l z3{@P8tQzC}$ae-T0Vj(cs{=s z0d;+ws}SreeGzuhSh!C4r@tbxcm92%(TfwE`~)i%w;IML4mOHA`4bfGH>jt3&RU;8 zDXzXuKXspwF4KV?yIs}y4@6YsKxb!Pgoz@r!+8OCk1qARa&!dZD2HNVM?^lfBn2ma=j!WjQ0!U_Hzy#c379I+Voxx!>}bYdi#WGA>TXUxuKCXX=mj$H&6 J>Dir$zX1aBke~nn delta 1665 zcmaKsT}+!*7{@tnDWip@PA4Pur7fjE8DHaLVB+RhpwLCS*;Xc-_*$W3Z1gQ2r~)^- zn2eZdLh@u;;`gQ|V=8m2iNeq+M$ODJqfsx;#S)X<=!NsbD>c*eKd%Ec#$Np1=lwrl z=RD`Uy>^m)eUcSrii;h2^0#++^T>swKeiNiH6G5SQ;kP5PfRC`+@{i>Eldz|4@e$# zKV%cu&~9R$GWy1ywF_Ict208nJ3w4(QLk~l`steHpo@OZF)vGVmxQ1{wXLk4G|fl% zm$$MyI#wP~86lNno44>vcpc~0Qf`$WV3joBc}6BK_ZwU)WWTl1Sl&K$Yz6Ch{fS9r87p&r6`HAmvw) zW>8(l>}KV_kj(6L-WwH5|Kt72e-GP9g_S$leQ0R^4kdQUMQKu=U6rrjmhU2ucLP}i zrS^3#b_N2@-H0dvwy8ot!t9iVRECA`riDN=gU}!cg}v2P9k^Wt1AnK=?-ABS=YF6E z2nlOvUrboTKxA)B(9Nm_btom&Rvlf}G9(L@(3{n7*<)y%p#GXSSU>$x!$E9z^L&?%ITB*I+sU!4JASofwdIz7InI`8uvgQBHP#;y`Tbp8`yyOz zQI23(<2y(gZUhB+eJX|TJg(tCGz;Ls|YBF%3mU!ke|&Gf-Xr! hqj(nux;~s4(sh1DY9imrrpKo_X2%zSt2DDG@fT=LFh=?BtFDe#11R-Lvj(0-4AG!mdHdAN=VeHOjSlRmD5}m zv``~DQY9@zEn6g2{!PM2Mf~K3TX799yqC^ag)ckct!;Rqs!{r+^j@dALSB>dT3u}pk0`07cpY}724wi^3 z);l4s$-<+_BZ`PoL>cgrn60QIeopHS71W#Kllp3L`*?qe5H>$_+``eplK`i=sv-Dc e4pG{gvFL%Z!}Z|DXA0Y1!g=&<Aw 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 VQNPH2Dg9))DE(yg^zd*g_kVl3xqkov literal 0 HcmV?d00001 diff --git a/easi_tools/__pycache__/load_s2l2a.cpython-310.pyc b/easi_tools/__pycache__/load_s2l2a.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7671a64289b407dec5ac4a88cfd494644a1c02c0 GIT binary patch literal 6624 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 W*COV^nX|J-!%_$^zOu}T-~0~@WuR;T literal 0 HcmV?d00001 diff --git a/easi_tools/__pycache__/notebook_utils.cpython-310.pyc b/easi_tools/__pycache__/notebook_utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6dcc74f80154f64d2c7f8773de6de98e739d783f GIT binary patch literal 5152 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 IXR2=gKl@Q}CjbBd literal 0 HcmV?d00001 diff --git a/easi_tools/deployments.py b/easi_tools/deployments.py new file mode 100644 index 0000000..f2e753f --- /dev/null +++ b/easi_tools/deployments.py @@ -0,0 +1,335 @@ +#!python3 + +import sys +import os +import logging +import collections + +# A class that provides notebook variables for each of the EASI deployments + +# Map an internal deployment name to deployment variables and search parameters. +# Update to ensure that the product/space/time parameters are available in the respective databases +deployment_map = { + 'adias': { + 'domain': 'adias.aquawatchaus.space', + 'db_database': 'adias_prod_db', + 'training_shapefile': '', + 'scratch': 'adias-prod-user-scratch', + 'productmap': {'landsat': 'landsat8_c2l2_sr', 'sentinel-2': 's2_l2a', 'sar': 'asf_s1_grd_gamma0', 'dem': 'copernicus_dem_30'}, + 'location': 'Lake Tahoe, California', + 'latitude': (39.0, 39.3), + 'longitude': (-120.2, -119.9), + 'time': ('2022-02-01', '2022-05-01'), + 'target': { + 'landsat': {'crs': 'epsg:26911', 'resolution': (-30,30)}, + 'sentinel-2': {'crs': 'epsg:26911', 'resolution': (-10,10)} + }, + 'aliases': { + 'landsat': {'qa_band': 'qa_pixel', 'nir': 'nir08', 'swir1': 'swir16', 'swir2': 'swir22'} + }, + 'qa_mask': { + 'landsat': {'nodata': False, 'water': 'land_or_cloud', + 'cloud': 'not_high_confidence', 'cloud_shadow': 'not_high_confidence'} + } + }, + 'asia': { + 'domain': 'asia.easi-eo.solutions', + 'db_database': 'easi_asia_db', + 'training_shapefile': '', + 'scratch': 'easi-asia-user-scratch', + 'productmap': {'landsat': 'landsat8_c2l2_sr', 'sentinel-2': 'sentinel_2_c1_l2a', 'sentinel-1': 'sentinel1_grd_gamma0_20m', 'dem': 'copernicus_dem_30'}, + 'location': 'Lake Tempe, Indonesia', + 'latitude': (-4.2, -3.9), + 'longitude': (119.8, 120.1), + 'time': ('2020-02-01', '2020-04-01'), + 'proxy': True, + 'target': { + 'landsat': {'crs': 'epsg:32650', 'resolution': (-30,30)}, + 'sentinel-2': {'crs': 'epsg:32650', 'resolution': (-10,10)} + }, + 'aliases': { + 'landsat': {'qa_band': 'qa_pixel', 'nir': 'nir08', 'swir1': 'swir16', 'swir2': 'swir22'} + }, + 'qa_mask': { + 'landsat': {'nodata': False, 'water': 'land_or_cloud', + 'cloud': 'not_high_confidence', 'cloud_shadow': 'not_high_confidence'} + } + }, + 'chile': { + 'domain': 'datacubechile.cl', + 'db_database': 'easido_prod_db', + 'training_shapefile': '', + 'scratch': 'easido-prod-user-scratch', + 'productmap': {'landsat': 'landsat8_c2l2_sr', 'sentinel-2': 'sentinel_2_c1_l2a', 'sar': 'asf_s1_grd_gamma0', 'dem': 'copernicus_dem_30'}, + 'location': 'La Serena, Chile', + 'latitude': (-29.95, -29.85), + 'longitude': (-71.3, -71.2), + 'latitude_big': (-29.95, -27.95), + 'longitude_big': (-71.3, -69.3), + 'time': ('2022-02-01', '2022-05-01'), + 'target': { + 'landsat': {'crs': 'epsg:32718', 'resolution': (-30,30)}, + 'sentinel-2': {'crs': 'epsg:32718', 'resolution': (-10,10)} + }, + 'aliases': { + 'landsat': {'qa_band': 'qa_pixel', 'nir': 'nir08', 'swir1': 'swir16', 'swir2': 'swir22'} + }, + 'qa_mask': { + 'landsat': {'nodata': False, 'water': 'land_or_cloud', + 'cloud': 'not_high_confidence', 'cloud_shadow': 'not_high_confidence'} + } + }, + 'cal': { + 'domain': 'cal.ceos.org', + 'db_database': 'ceoseail_eail_db', + 'training_shapefile': './ancillary_data/VA_Counties_Newport_News.shp', + 'scratch': 'ceoseail-eail-user-scratch', + 'ows': False, + 'map': False, + 'productmap': {'landsat': 'landsat8_c2l2_sr', 'sentinel-2': 's2_l2a', 'sentinel-1': 's1_rtc', 'dem': 'copernicus_dem_30'}, + 'location': 'Newport News, Virginia', + 'latitude': (37.02, 37.12), + 'longitude': (-76.55, -76.45), + 'time': ('2022-01-01', '2022-04-01'), + 'target': { + 'landsat': {'crs': 'epsg:32618', 'resolution': (-30,30)}, + 'sentinel-2': {'crs': 'epsg:32618', 'resolution': (-10,10)} + }, + 'aliases': { + 'landsat': {'qa_band': 'qa_pixel', 'nir': 'nir08', 'swir1': 'swir16', 'swir2': 'swir22'} + }, + 'qa_mask': { + 'landsat': {'nodata': False, 'water': 'land_or_cloud', + 'cloud': 'not_high_confidence', 'cloud_shadow': 'not_high_confidence'} + } + }, + 'csiro': { + 'domain': 'csiro.easi-eo.solutions', + 'db_database': 'easihub_csiro_db', + 'training_shapefile': '', + 'scratch': 'easihub-csiro-user-scratch', + 'productmap': {'landsat': 'ga_ls8c_ard_3', 'sentinel-2': 'ga_s2am_ard_3', 'sentinel-1': 'sentinel1_grd_gamma0_20m', 'dem': 'copernicus_dem_30'}, + 'location': 'Lake Hume, Australia', + 'latitude': (-36.3, -35.8), + 'longitude': (146.8, 147.3), + 'time': ('2020-02-01', '2020-04-01'), + 'aliases': { + 'landsat': {'red': 'nbart_red', 'green': 'nbart_green', 'blue': 'nbart_blue', + 'nir': 'nbart_nir', 'swir1': 'nbart_swir_1', 'swir2': 'nbart_swir_2', + 'qa_band': 'oa_fmask'} + }, + 'qa_mask': { + 'landsat': {'fmask':'valid'} + } + }, + 'sub-apse2': { + 'domain': 'sub-apse2.easi-eo.solutions', + 'db_database': '', + 'training_shapefile': '', + 'scratch': '', + 'ows': False, + 'map': False, + 'productmap': {'landsat': 'ga_ls8c_ard_3', 'sentinel-2': 'ga_s2am_ard_3', 'dem': 'copernicus_dem_30'}, + 'location': 'Lake Hume, Australia', + 'latitude': (-36.3, -35.8), + 'longitude': (146.8, 147.3), + 'time': ('2020-02-01', '2020-04-01'), + 'aliases': { + 'landsat': {'red': 'nbart_red', 'green': 'nbart_green', 'blue': 'nbart_blue', + 'nir': 'nbart_nir', 'swir1': 'nbart_swir_1', 'swir2': 'nbart_swir_2', + 'qa_band': 'oa_fmask'} + }, + 'qa_mask': { + 'landsat': {'fmask':'valid'} + } + }, +} + + +class EasiDefaults(): + """Provide deployment-specific default variables for EASI notebooks""" + + def __init__(self, deployment=None): + """Initialise""" + self._log = _getlogger(self.__class__.__name__) + self.name = deployment if deployment else self._find_deployment() + self.deployment = self._validate(self.name) + self.proxy = None + self._aliases = {} + if self.deployment and self.deployment.get('proxy', None): + self.proxy = EasiCachingProxy() + if self.deployment: + self._log.info(f'Successfully found configuration for deployment "{self.name}"') + + def _validate(self, deployment) -> dict: + """Return the dict associated with the deployment name""" + names = deployment_map.keys() + if deployment is None or deployment not in names: + self._log.error(f'Deployment name not recognised: {deployment}') + self._log.error(f'Select one of: {", ".join(names)}') + return None + return deployment_map[deployment] + + def _find_deployment(self) -> str: + """Use the deployment's database environment variable as a lookup into the deployment_map dict""" + db_database = os.environ['DB_DATABASE'] + deployment_name = [item for item in deployment_map if deployment_map[item]["db_database"] == db_database] + msg = 'Try specifying one using EasiDefaults(deployment="deployment_name").' + if len(deployment_name) == 0: + self._log.error(f'Deployment could not be found automatically. {msg}') + return None + elif len(deployment_name) > 1: + self._log.error(f'More than one deployment found. {msg}') + return None + return deployment_name[0] + + + @property + def domain(self): + """Deployment domain""" + return self.deployment['domain'] + + @property + def db_database(self): + """Database name""" + return self.deployment['db_database'] + + @property + def training_shapefile(self): + """A local shapefile""" + return self.deployment['training_shapefile'] + + @property + def hub(self): + """JupyterLab URL""" + return f'https://hub.{self.domain}' + + @property + def explorer(self): + """Explorer URL""" + return f'https://explorer.{self.domain}' + + @property + def ows(self): + """OWS URL""" + if not self.deployment.get('ows', True): + self._log.warning(f'Deployment does not have an OWS service: {self.name}') + return None + return f'https://ows.{self.domain}' + + @property + def terria(self): + """Terria Map URL""" + if not self.deployment.get('map', True): + self._log.warning(f'Deployment does not have a Map service: {self.name}') + return None + return f'https://map.{self._domain()}' + + @property + def scratch(self): + """Scratch bucket""" + return self.deployment['scratch'] + + @property + def location(self): + """Default location name""" + return self.deployment['location'] + + @property + def latitude(self): + """Default latitude range""" + return self.deployment['latitude'] + + @property + def longitude(self): + """Default longitude range""" + return self.deployment['longitude'] + + @property + def latitude_big(self): + """Default big latitude range""" + if 'latitude_big' in self.deployment: + return self.deployment['latitude_big'] + self._log.warning(f'Default big latitude range not defined for "{self.deployment}". Using default latitude range') + return self.latitude + + @property + def longitude_big(self): + """Default big longitude range""" + if 'longitude_big' in self.deployment: + return self.deployment['longitude_big'] + self._log.warning(f'Default big longitude range not defined for "{self.deployment}". Using default longitude range') + return self.latitude + + @property + def time(self): + """Default time range""" + return self.deployment['time'] + + def product(self, family='landsat'): + """Product name. Family loosely describes products from a satellite series or product type.""" + p = self.deployment['productmap'].get(family, None) + if p is None: + self._log.warning(f'Product family not defined for "{self.name}": {family}') + out = ', '.join([f'{k} > {v}' for k,v in self.deployment['productmap'].items()]) + self._log.warning(f'{self.name}: {out}') + return None + return p + + def crs(self, family='landsat'): + """Default resolution. Family loosely describes products from a satellite series or product type.""" + return self.deployment.get('target', {}).get(family, {}).get('crs', None) + + def resolution(self, family='landsat'): + """Default resolution. Family loosely describes products from a satellite series or product type.""" + return self.deployment.get('target', {}).get(family, {}).get('resolution', None) + + def aliases(self, family='landsat') -> collections.UserDict: + """Return a dict-like object that maps a common name to a specific measurement/alias name. + Family loosely describes products from a satellite series or product type. + + The common name is returned if there is no specific measurement/alias name defined. + That is, the common name should work as a measurement/alias name for the family in this deployment. + Else, provide a specific measurement/alias name in the defaults above. + """ + if family not in self._aliases: + self._aliases[family] = EasiAlias(self.deployment.get('aliases', {}).get(family, {})) + return self._aliases[family] + + def qa_mask(self, family='landsat') -> dict: + """Default QA mask values. Family loosely describes products from a satellite series or product type.""" + return self.deployment.get('qa_mask', {}).get(family, {}) + + +class EasiAlias(collections.UserDict): + """Custom UserDict that returns a default measurement name for a given key if defined. + Else returns the key as the value. Items can not be set.""" + def __init__(self, default:dict = {}): + self.data = default + self._log = _getlogger(self.__class__.__name__) + def __getitem__(self, key): + if key in self.data: + return self.data[key] + return key + def __setitem__(self, key, val): + self._log.error(f'Error <{self.__class__.__name__}>: Can not set items') + + +class EasiCachingProxy(): + """Set, unset and return information about the user's caching-proxy configuration""" + def __init__(self): + pass + + +def _getlogger(name): + """Return a logger. Define here to limit external dependencies""" + # Default logger + # log.hasHandlers() = False + # log.getEffectiveLevel() = 30 = warning + # log.propagate = True + logger = logging.getLogger(name) + logger.setLevel(logging.INFO) + if not len(logger.handlers): + logger.addHandler(logging.StreamHandler(sys.stdout)) + logger.propagate = False # Do not propagate up to root logger, which may have other handlers + return logger diff --git a/easi_tools/load_s2l2a.py b/easi_tools/load_s2l2a.py new file mode 100644 index 0000000..918a1c7 --- /dev/null +++ b/easi_tools/load_s2l2a.py @@ -0,0 +1,293 @@ +#!python + +# Sentinel-2 L2A Collection 0 scaling and offset corrections. +# - Applies to data indexed from https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a +# - The newer https://earth-search.aws.element84.com/v1/collections/sentinel-2-c1-l2a (Collection 1) may not be affected in the same way +# +# TL;DR: +# DN values in COG files have different definitions depending on the processing baseline version +# and whether the offset change has been pre-applied by the cloud data custodian. +# +# Background: +# +# ESA has undertaken a reprocessing of the Sentinel-2 L2A product that includes +# a change to the offset value used to convert digital numbers (in file) to +# scientific values (reflectances). +# +# https://sentinels.copernicus.eu/web/sentinel/technical-guides/sentinel-2-msi/level-2a-algorithms-products +# +# L2A algorithm and products: Starting with the PB 04.00 (25th January 2022), the dynamic +# range of the Level-2A products is shifted by a band-dependent constant: BOA_ADD_OFFSET. +# This offset will allow encoding negative surface reflectances that may occur over very +# dark surfaces. +# +# L2A_SRi = (L2A_DNi + BOA_ADD_OFFSETi) / QUANTIFICATION_VALUEi +# +# QUANTIFICATION_VALUEi = 10000 +# BOA_ADD_OFFSETi = -1000 +# +# refl = (dn -1000) / 10000 +# refl = dn/10000 - 1000/10000 +# refl = dn * 0.0001 - 0.1 +# +# These are the values in the EASI product definition, e.g. +# https://explorer.asia.easi-eo.solutions/products/s2_l2a.odc-product.yaml +# +# Example workflow: +# +# ESA's reprocessing is flowing through to the AWS open data repository of S2 L2A but +# while this stabilises we may see inconsistencies in time series queries due to: +# - More than one processed version of a dataset (scene) in the AWS bucket and indexed in an EASI database +# - Datasets (scenes) that indicate they have an offset applied by ESA but the offset correction +# has been not been applied to the COG +# +# Element-84 discussion: +# https://github.com/Element84/earth-search/issues/23#issuecomment-1834674853 + + +import xarray as xr +import pandas as pd +import logging +from pathlib import Path +import sys, re + +import datacube +from datacube.api.core import output_geobox +from datacube.api.query import SPATIAL_KEYS, CRS_KEYS, OTHER_KEYS +from datacube.utils import masking + + +# Set logger +log = logging.getLogger(Path(__file__).stem) +log.setLevel(logging.INFO) +log.addHandler(logging.StreamHandler(sys.stdout)) + +# Constants +search_keys = ( + 'product', + 'time', + 'geopolygon', + 'like', + 'limit', + 'ensure_location', + 'dataset_predicate', +) + SPATIAL_KEYS + CRS_KEYS + OTHER_KEYS + +# TODO: get measurement aliases from the ODC product record +refl_bands = { + 'coastal','band_01','B01','coastal_aerosol', + 'blue','band_02','B02', + 'green','band_03','B03', + 'red','band_04','B04', + 'rededge1','band_05','B05','red_edge_1', + 'rededge2','band_06','B06','red_edge_2', + 'rededge3','band_07','B07','red_edge_3', + 'nir','band_08','B08','nir_1', + 'nir08','band_8a','B8A','nir_2', + 'nir09','band_09','B09','nir_3', + 'swir16','band_11','B11','swir_1','swir_16', + 'swir22','band_12','B12','swir_2','swir_22', +} +scale_factor = 0.0001 +add_offset = -0.1 + + +def highest_sequence_number(matches: list) -> dict: + """Filter for the highest element84 processing sequence number per scene (scene label excluding the sequence number) + + : return : { scene_id_excluding_sequence_number : { highest_sequence_number : datacube.model.Dataset }} + """ + p = re.compile(r'(S2.+)_([0-9]+)_(L2A)') + sorter = {} + for ds in matches: + # Separate the scene label from the sequence number + label = ds.metadata_doc['label'] + m = p.match(label) + if not m: + log.warning(f'Dataset label does not match expected pattern: {label}') + continue + key = f'{m.group(1)}_{m.group(3)}' + seq = int(m.group(2)) + # Retain the highest sequence number + if key in sorter: + if list(sorter[key])[0] < seq: + sorter[key] = {seq: ds} + else: + sorter[key] = {seq: ds} + return sorter + + +def ds_requires_offset(ds: datacube.model.Dataset) -> bool: + """Return True if a dataset's metadata indicates that the offset correction should be applied""" + props = ds.metadata_doc['properties'] + + # If baseline is less than '04.00' then offset correction does not apply + baseline = props.get('s2:processing_baseline', '0.0') + p = re.compile(r'(\d+)\.(\d+)') + m = p.match(baseline) + if not m: + log.warning(f'Dataset processing_baseline does not match expected pattern: {baseline}') + return None + if int(m.group(1)) < 4: + return False + + # If the boa_offset_applied has been applied then offset correction is not required + boa_offset_applied = props.get('earthsearch:boa_offset_applied', False) + return not boa_offset_applied + + +def apply_correction_to_data(ds: xr.Dataset, offset: float = 0) -> xr.Dataset: + """Apply the scale and offset correction to each reflectance band where there is valid data (not nodata)""" + refl_vars = [x for x in ds.data_vars if x in refl_bands] + mask = masking.valid_data_mask(ds[refl_vars]) + # Save on a dask step? + if offset == 0: + ds[refl_vars] = ds[refl_vars].where(mask) * scale_factor + else: + ds[refl_vars] = ds[refl_vars].where(mask) * scale_factor + offset + return ds + + +def load_s2l2a_with_offset( + dc: datacube.Datacube, + query: dict, +) -> xr.Dataset: + """ + Replaces datacube.load(**query) for s2_l2a products. + + Method: + - Find all datasets matching the query (dc.find_datasets) + - Filter for the highest element84 processing sequence number per scene (scene label excluding the sequence number) + - Filter into two lists for datasets that have + - "s2:processing_baseline" >= "04.00" and "earthsearch:boa_offset_applied" == False (offset correction required) + - everything else (no correction required) + - If either list is empty then load the non-empty list, apply scale (and offset if required), and return the xarray Dataset + - Load and combine the two lists of datasets + - Load each list, apply scale (and offset if required) + - Concat on time dimension and sort by time + - Return the combined xarray Dataset + + Notes: + - Any 'groupby' function is applied to each of the xarray Datasets prior to them being combined. + This could create "extra" (non-grouped) time layers in the combined Dataset if the groupby function + would have grouped datasets (scenes) from both lists. + - Scale and offset are applied to the reflectance bands where there is valid data (not `nodata`). + This includes applying the "scale_factor" even if no datasets require the offset correction. + Other masks can be applied by the user (e.g. pixel quality or cloud masking). + """ + + product = query.get('product', '') + if product != 's2_l2a': + log.error(f'This function only applies to the "s2_l2a" product, not: {product}') + return None + + # Find all datasets matching the query + matches = None + if 'datasets' in query: + matches = query['datasets'] + del query['datasets'] + if matches is None: + search_params = {k:v for k,v in query.items() if k in search_keys} + matches = dc.find_datasets(**search_params) + if 'skip_broken_datasets' not in query: + # This helps to avoid data loading error messages + query['skip_broken_datasets'] = True + + # Filter for the highest element84 processing sequence number + sorter = highest_sequence_number(matches) + + # Filter into two lists + offset_applied, offset_required = [], [] + for key in sorter.keys(): + ds = list(sorter[key].values())[0] + isrequired = ds_requires_offset(ds) + if isrequired is None: + continue + elif isrequired: + offset_required.append(ds) + else: + offset_applied.append(ds) + matches_combined = offset_applied + offset_required + + # If either list is empty then no separation and merge is required + this_offset = None + if len(offset_applied) == 0: + log.info('All datasets require offset correction') + msg = 'The valid_data_mask, scale and offset have been applied to the reflectance bands' + this_offset = add_offset + if len(offset_required) == 0: + log.info('No datasets require offset correction') + msg = 'The valid_data_mask and scale (no offset) have been applied to the reflectance bands' + this_offset = 0 + if this_offset is not None: + data = dc.load( + datasets = matches_combined, + **query + ) + xx = apply_correction_to_data(data, this_offset) + log.info(msg) + return xx + + # DEBUG: What do we have + # def func(s): + # p = re.compile('(\d{8})') + # m = p.search(s[0]) + # if m: + # return m.group(1) + # log.info(f'Number of datasets in initial query: {len(matches)}') + # log.info(f'{sorted([(x.metadata_doc["label"],x.id) for x in matches], key=func)}') + # log.info(f'Number of datasets with offset applied: {len(offset_applied)}') + # log.info(f'{sorted([(x.metadata_doc["label"],x.id) for x in offset_applied], key=func)}') + # log.info(f'Number of datasets without offset applied: {len(offset_required)}') + # log.info(f'{sorted( [(x.metadata_doc["label"],x.id) for x in offset_required], key=func)}') + # return + + # Else, load data into two Datasets + log.info('Mix of datasets found with either offset required or not.') + log.info('We will load two xarrays, apply offset where required, and merge into one xarray.') + + # 1. Ensure the target geobox covers all datasets + target_geobox = output_geobox( + datasets = matches_combined, + **query, + ) + + # 2. Edit the query for our needs + # Ensure that dask time chunking = 1 + dask_input = None + if 'dask_chunks' in query: + dask_input = query['dask_chunks'] # Save + if dask_input.get('time', 1) != 1: + query['dask_chunks'].update({'time': 1}) + # Remove keys that are not compatible with 'like' + for x in ('output_crs', 'resolution', 'align'): + if x in query: + del query[x] + + # 3. Load two xarrays + data_offset_applied = dc.load( + datasets = offset_applied, + like = target_geobox, + **query + ) + data_offset_required = dc.load( + datasets = offset_required, + like = target_geobox, + **query + ) + + # 4. Apply respective scale and offsets + data_offset_applied = apply_correction_to_data(data_offset_applied) + data_offset_required = apply_correction_to_data(data_offset_required, add_offset) + + # 5. Combine the two xarrays + combined = xr.concat([data_offset_applied, data_offset_required], dim='time') + combined = combined.sortby('time') + + # 6. Reapply any time > 1 chunking + if dask_input is not None: + if dask_input.get('time', 1) != 1: + combined = combined.chunk(dask_input) + + log.info('The valid_data_mask, scale and offset have been applied to the reflectance bands') + return combined diff --git a/easi_tools/notebook_utils.py b/easi_tools/notebook_utils.py new file mode 100644 index 0000000..4f2ba13 --- /dev/null +++ b/easi_tools/notebook_utils.py @@ -0,0 +1,171 @@ +#!python3 + +# A collection of utilities that can be used in Python notebooks. +# +# License: Apache 2.0 + +# Created for EASI Hub training notebooks, https://dev.azure.com/csiro-easi/easi-hub-public/_git/hub-notebooks + +# Data tools +import numpy as np +import xarray as xr +import pandas as pd +import geopandas as gpd +import datacube +from datacube.utils import masking +from datetime import datetime + +# hvPlot, Holoviews, Datashader and Bokeh +import hvplot.pandas +import hvplot.xarray +import panel as pn +import holoviews as hv +# hv.extension("bokeh", logo=False) # Its likely set from in the notebooks + +# Jupyter Lab +from IPython.display import HTML + +# Python +import sys, os, re +import logging +from pathlib import Path +from collections import Counter +import contextlib + +# Dask +import dask +from dask.distributed import Client, LocalCluster +from dask_gateway import Gateway + +# EASIDefaults +from . import EasiDefaults + +# Set logger +logger = logging.getLogger(Path(__file__).stem) +logger.setLevel(logging.INFO) +if not len(logger.handlers): + logger.addHandler(logging.StreamHandler(sys.stdout)) + + +def display_table( + df: pd.DataFrame, + panel: bool = False, +): + """Display the full pandas dataframe. If panel is True use a panel object""" + table = None + if panel: + # Dicts are rendered as "[object Object]". Need to set a formatter, I guess. + table = pn.widgets.DataFrame(df, + # sizing_mode='stretch_width', # equal column widths, full screen + autosize_mode='fit_viewport', # fitted columns, about 90-95% width + # reorderable=True, # didn't work first try + ) + else: + with pd.option_context("display.max_rows", None, + "display.max_columns", None, + "display.max_colwidth", -1): + table = HTML( df.to_html().replace(r"\n", "
") ) + display(table) + + +def heading(txt: str): + """Print a simple HTML heading""" + display(HTML( f"

{txt}

" )) + + +def hv_table_hook(plot, element): + """Selected options for hv.table() formatting + + Use: df.hv.table().opts(hooks=[hv_table_hook]) + """ + plot.handles["table"].autosize_mode="fit_viewport" + # Other examples + # plot.handles['table'].row_height = 40 + # from bokeh.models.widgets import DateFormatter + # plot.handles['table'].columns[6].formatter = DateFormatter(format='%Y-%m-%d') + + +def xarray_object_size(data): + """Return a formatted string""" + val, unit = data.nbytes / (1024 ** 2), "MB" + if val > 1024: + val, unit = data.nbytes / (1024 ** 3), "GB" + return f"Dataset size: {val:.2f} {unit}" + + +def mostcommon_crs(dc, query): + """Adapted from https://github.com/GeoscienceAustralia/dea-notebooks/blob/develop/Tools/dea_tools/datahandling.py""" + matching_datasets = dc.find_datasets(**query) + crs_list = [str(i.crs) for i in matching_datasets] + crs_mostcommon = None + if len(crs_list) > 0: + # Identify most common CRS + crs_counts = Counter(crs_list) + crs_mostcommon = crs_counts.most_common(1)[0][0] + else: + logger.warning("No data was found for the supplied product query") + return crs_mostcommon + + +def initialize_dask(use_gateway=False, workers=(1,2), wait=False, local_port=8786, **kwargs): + """Initialize a Dask Gateway or Local cluster""" + # Check inputs + if isinstance(workers, (int, float)): + workers = (int(workers), int(workers)) + if len(workers) != 2: + logger.error("Require workers to be a single integer or a 2-element tuple/list") + return None, None + if isinstance(local_port, (str, float)): + local_port = int(local_port) + + # Dask gateway + if use_gateway: + gateway = Gateway() + clusters = gateway.list_clusters() + if not clusters: + logger.info("Starting new cluster") + cluster = gateway.new_cluster(**kwargs) + else: + logger.info(f"An existing cluster was found. Connecting to: {clusters[0].name}") + cluster = gateway.connect(clusters[0].name) + client = cluster.get_client() + cluster.adapt(minimum=workers[0], maximum=workers[1]) + if wait: + logger.info("Waiting for at least one cluster worker") + # client.wait_for_workers(n_workers=1) # Before release 2023.10.0 + client.sync(client._wait_for_workers,n_workers=1) # Since release 2023.10.0 + + # Local cluster + else: + cluster = LocalCluster(n_workers=4) + client = Client(cluster) + server = f'https://hub.{EasiDefaults().domain}' # Or replace if not using EasiDefaults + user = os.environ.get('JUPYTERHUB_SERVICE_PREFIX') # Current user + dask.config.set({"distributed.dashboard.link": f'{server}{user}' + "proxy/{port}/status"}) # port is evaluated by dask + + return cluster, client + + +def localcluster_dashboard(client, server="https://hub.csiro.easi-eo.solutions"): + """Return a dashboard link using jupyter proxy""" + dashboard_link = client.dashboard_link + for host in ("127.0.0.1", "localhost"): + if host in dashboard_link: + port = re.search(r":(\d+)\/status", dashboard_link).group(1) + dashboard_link = f'{server}{os.environ["JUPYTERHUB_SERVICE_PREFIX"]}proxy/{port}/status' + break + return dashboard_link + + +@contextlib.contextmanager +def unset_cachingproxy(): + """Unset the EASI caching proxy with a context manager""" + # Inspired by https://stackoverflow.com/a/34333710 + env = os.environ + remove = ("AWS_HTTPS", "GDAL_HTTP_PROXY") + update_after = {k: env[k] for k in remove} + try: + [env.pop(k, None) for k in remove] + yield + finally: + env.update(update_after) diff --git a/new_import_ODC.py b/new_import_ODC.py index 0845f48..b0dfb97 100644 --- a/new_import_ODC.py +++ b/new_import_ODC.py @@ -82,47 +82,87 @@ import joblib def load_data(dc, date_range, longtitude_range, latitude_range): + """ + Load Sentinel-2 L2A data using direct datacube.load() + without spatial filtering (which was causing 0 results). + + Note: Data is loaded in UTM (EPSG:32648) to avoid CRS issues. + Spatial filtering on lat/lon is skipped to return maximum data. + """ product = 's2_l2a' - query = { - 'product': product, # Product name - 'x': longtitude_range, # "x" axis bounds - 'y': latitude_range, # "y" axis bounds - 'time': date_range, # Any parsable date strings - } - native_crs = notebook_utils.mostcommon_crs(dc, query) - print(f'Most common native CRS: {native_crs}') + native_crs = 'EPSG:32648' # UTM Zone 48N for Vietnam measurements = ['red', 'nir', 'scl'] - - load_params = { - 'measurements': measurements, # Selected measurement or alias names - 'output_crs': native_crs, # Target EPSG code - 'resolution': (-10, 10), # Target resolution - 'group_by': 'solar_day', # Scene grouping - 'dask_chunks': {'x': 2048, 'y': 2048}, # Dask chunks - } - data = load_s2l2a_with_offset( - dc, - query | load_params # Combine the two dicts that contain our search and load parameters - ) - return data + + print(f'Loading Sentinel-2 data (EPSG:32648)...') + print(f' Time range: {date_range}') + print(f' Measurements: {measurements}') + + try: + # Load ALL available data WITHOUT dask_chunks (forces immediate load) + # This avoids the metadata issue with dc.load() when using dask_chunks + data = dc.load( + product=product, + time=date_range, + measurements=measurements, + output_crs=native_crs, + resolution=(-10, 10), + group_by='solar_day', + skip_broken_datasets=True + ) + + print(f'✅ Data loaded successfully!') + print(f' Dimensions: {dict(data.sizes)}') + print(f' Time steps: {len(data.time)}') + print(f' Spatial extent: x={len(data.x)}, y={len(data.y)}') + print(f' Data type: numpy arrays (not Dask)') + + return data + + except Exception as e: + print(f'❌ Error loading data: {e}') + import traceback + traceback.print_exc() + return None def mask_clean(data): - flag_name = 'scl' - flag_desc = masking.describe_variable_flags(data[flag_name]) # Pandas dataframe - display(flag_desc) - display(flag_desc.loc['qa'].values[1]) - # Create a "data quality" Mask layer - flags_def = flag_desc.loc['qa'].values[1] - good_pixel_flags = [flags_def[str(i)] for i in [2, 4, 5, 6]] # To pass strings to enum_to_bool() - - # enum_to_bool calculates the pixel-wise "or" of each set of pixels given by good_pixel_flags - # 1 = good data - # 0 = "bad" data - good_pixel_mask = enum_to_bool(data[flag_name], good_pixel_flags) + """ + Clean data by masking clouds and bad pixels using the SCL (Scene Classification Layer). + + SCL classes: + - 0: No Data + - 1: Saturated/Defective + - 2: Dark Area Pixels + - 3: Cloud Shadows + - 4: Vegetation ✓ GOOD + - 5: Not Vegetated ✓ GOOD + - 6: Water ✓ GOOD + - 7: Unclassified ✓ GOOD + - 8: Cloud Medium Probability ✗ BAD + - 9: Cloud High Probability ✗ BAD + - 10: Thin Cirrus ✗ BAD + - 11: Snow/Ice ✗ BAD + """ + + # Good pixel classes (keep these) + good_pixel_classes = [4, 5, 6, 7] + + # Create mask: 1 where SCL is in good_pixel_classes, 0 otherwise + good_pixel_mask = data['scl'].isin(good_pixel_classes) + + print(f'✅ Cloud masking applied') + print(f' Good pixel classes: {good_pixel_classes}') + print(f' Mask created (dask-backed, not yet computed)') + + # Get all variables except SCL data_layer_names = [x for x in data.data_vars if x != 'scl'] - # Apply good pixel mask to blue, green, red and nir. + + # Apply mask to all layers result = data[data_layer_names].where(good_pixel_mask).persist() + + print(f' Data variables masked: {data_layer_names}') + print(f' Result persisted to workers') + return result @@ -360,7 +400,17 @@ def load_data_sen2(dc, date_range, coordinates): 'y': latitude_range, # "y" axis bounds 'time': date_range, # Any parsable date strings } - native_crs = notebook_utils.mostcommon_crs(dc, query) + + # Try to get native CRS, default to EPSG:32648 (UTM Zone 48N) for Vietnam + try: + native_crs = notebook_utils.mostcommon_crs(dc, query) + if native_crs is None: + print('⚠️ Could not determine native CRS, using EPSG:32648 (UTM Zone 48N)') + native_crs = 'EPSG:32648' + except Exception as e: + print(f'⚠️ Error determining CRS: {e}, using EPSG:32648') + native_crs = 'EPSG:32648' + print(f'Most common native CRS: {native_crs}') # measurements = ['red','green', 'blue', 'nir', 'scl'] @@ -373,10 +423,27 @@ def load_data_sen2(dc, date_range, coordinates): 'group_by': 'solar_day', # Scene grouping 'dask_chunks': {'x': 2048, 'y': 2048}, # Dask chunks } - data = load_s2l2a_with_offset( - dc, - query | load_params # Combine the two dicts that contain our search and load parameters - ) + + try: + data = load_s2l2a_with_offset( + dc, + query | load_params # Combine the two dicts that contain our search and load parameters + ) + except Exception as e: + print(f'❌ Error loading data: {e}') + print('Attempting direct dc.load without offset correction...') + data = dc.load( + product=product, + x=longtitude_range, + y=latitude_range, + time=date_range, + measurements=measurements, + output_crs=native_crs, + resolution=(-10, 10), + group_by='solar_day', + dask_chunks={'x': 2048, 'y': 2048}, + skip_broken_datasets=True + ) return data def mask_cloud(data):