From d6ba7b64ec1e6d923f2831133864b9aaa46d9949 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Wed, 21 Aug 2024 16:49:01 -0700 Subject: [PATCH 001/102] removed unused code --- src/rat/core/sarea/sarea_cli_l8.py | 7 ------- src/rat/core/sarea/sarea_cli_l9.py | 6 ------ src/rat/core/sarea/sarea_cli_s2.py | 5 ----- 3 files changed, 18 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_l8.py b/src/rat/core/sarea/sarea_cli_l8.py index 7e301d9e..84fb958e 100644 --- a/src/rat/core/sarea/sarea_cli_l8.py +++ b/src/rat/core/sarea/sarea_cli_l8.py @@ -6,7 +6,6 @@ import os from random import randint from itertools import zip_longest -from rat.ee_utils.ee_utils import poly2feature from rat.ee_utils.ee_utils import poly2feature from rat.utils.logging import LOG_NAME, NOTIFICATION @@ -333,9 +332,6 @@ def run_process_long(res_name, res_polygon, start, end, datadir): scratchdir = os.path.join(datadir, "_scratch") - # flag = True - num_runs = 0 - # If data already exists, only get new data starting from the last one savepath = os.path.join(datadir, f"{res_name}.csv") @@ -441,9 +437,6 @@ def run_process_long(res_name, res_polygon, start, end, datadir): elif df.index[-1].strftime('%Y-%m-%d') == fo: print(f"Reached last available observation - {fo}") break - elif num_runs > 1000: - print("Quitting: Reached 1000 iterations") - break except Exception as e: log.error(e) continue diff --git a/src/rat/core/sarea/sarea_cli_l9.py b/src/rat/core/sarea/sarea_cli_l9.py index 58962e5d..f5702bb9 100644 --- a/src/rat/core/sarea/sarea_cli_l9.py +++ b/src/rat/core/sarea/sarea_cli_l9.py @@ -8,7 +8,6 @@ from random import randint import argparse from itertools import zip_longest -from rat.ee_utils.ee_utils import poly2feature from rat.ee_utils.ee_utils import poly2feature from rat.utils.logging import LOG_NAME, NOTIFICATION @@ -339,8 +338,6 @@ def run_process_long(res_name, res_polygon, start, end, datadir): scratchdir = os.path.join(datadir, "_scratch") - # flag = True - num_runs = 0 # If data already exists, only get new data starting from the last one savepath = os.path.join(datadir, f"{res_name}.csv") @@ -447,9 +444,6 @@ def run_process_long(res_name, res_polygon, start, end, datadir): elif df.index[-1].strftime('%Y-%m-%d') == fo: print(f"Reached last available observation - {fo}") break - elif num_runs > 1000: - print("Quitting: Reached 1000 iterations") - break except Exception as e: log.error(e) continue diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 45dec1df..05858592 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -343,8 +343,6 @@ def run_process_long(res_name,res_polygon, start, end, datadir): scratchdir = os.path.join(datadir, "_scratch") - # flag = True - num_runs = 0 # If data already exists, only get new data starting from the last one savepath = os.path.join(datadir, f"{res_name}.csv") @@ -462,9 +460,6 @@ def run_process_long(res_name,res_polygon, start, end, datadir): elif df.index[-1].strftime('%Y-%m-%d') == fo: print(f"Reached last available observation - {fo}") break - elif num_runs > 1000: - print("Quitting: Reached 1000 iterations") - break except Exception as e: log.error(e) continue From f698045e2036a0d053778c80a9ffcfbfbb76f7d8 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Wed, 21 Aug 2024 16:49:23 -0700 Subject: [PATCH 002/102] replace type with geom_type due to deprecation --- src/rat/ee_utils/ee_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rat/ee_utils/ee_utils.py b/src/rat/ee_utils/ee_utils.py index 3eeaaf89..c29e0a6d 100644 --- a/src/rat/ee_utils/ee_utils.py +++ b/src/rat/ee_utils/ee_utils.py @@ -6,7 +6,7 @@ def poly2feature(polygon,buffer_distance): ''' Returns an earth engine feature with the same geometry as the polygon object with buffer_distance added. buffer_distance is in meters. ''' - if(polygon.type=='MultiPolygon'): + if(polygon.geom_type=='MultiPolygon'): all_cords=[] for poly in polygon.geoms: x,y = poly.exterior.coords.xy From 132f701842ddbf970987326d7a8dd9a6802e7f99 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Wed, 21 Aug 2024 16:51:01 -0700 Subject: [PATCH 003/102] Deletion of metsim input to save space & increase days limit for deletion of state file to 20. --- docs/Configuration/rat_config.md | 8 ++++---- src/rat/rat_basin.py | 6 +++--- src/rat/utils/clean.py | 27 ++++++++++++++++++++------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/docs/Configuration/rat_config.md b/docs/Configuration/rat_config.md index c57b370f..b2987c47 100644 --- a/docs/Configuration/rat_config.md +++ b/docs/Configuration/rat_config.md @@ -730,11 +730,11 @@ This section of the configuration file describes the parameters defined by `rout *
*`clean_metsim`* :
Required parameter - Description : `True` if you want to delete intermediate metsim outputs for a river basin after the RAT run. Otherwise, `False`. + Description : `True` if you want to delete intermediate metsim inputs and outputs for a river basin after the RAT run. Otherwise, `False`. Default : `False` - Syntax : If you want to delete intermediate metsim outputs for a river basin, + Syntax : If you want to delete intermediate metsim inputs and outputs for a river basin, ``` CLEAN_UP: clean_metsim: True @@ -743,7 +743,7 @@ This section of the configuration file describes the parameters defined by `rout *
*`clean_vic`* :
Required parameter - Description : `True` if you want to delete intermediate vic inputs and outputs, and any vic initial soil state file that is older than 15 days, for a river basin after the RAT run. Otherwise, `False`. + Description : `True` if you want to delete intermediate vic inputs and outputs, and any vic initial soil state file that is older than 20 days, for a river basin after the RAT run. Otherwise, `False`. Default : `False` @@ -756,7 +756,7 @@ This section of the configuration file describes the parameters defined by `rout *
*`clean_routing`* :
Required parameter - Description : `True` if you want to delete intermediate routing inputs and outputs, and any routing initial state file that is older than 15 days, for a river basin after the RAT run. Otherwise, `False`. + Description : `True` if you want to delete intermediate routing inputs and outputs, and any routing initial state file that is older than 20 days, for a river basin after the RAT run. Otherwise, `False`. Default : `False` diff --git a/src/rat/rat_basin.py b/src/rat/rat_basin.py index d5c09a27..b99223e6 100644 --- a/src/rat/rat_basin.py +++ b/src/rat/rat_basin.py @@ -851,13 +851,13 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base # Clearing out memory space as per user input if(config['CLEAN_UP'].get('clean_metsim')): - rat_logger.info("Clearing up memory space: Removal of metsim output files") + rat_logger.info("Clearing up memory space: Removal of metsim input and output files") cleaner.clean_metsim() if(config['CLEAN_UP'].get('clean_vic')): - rat_logger.info("Clearing up memory space: Removal of vic input, output files and previous init_state_files") + rat_logger.info("Clearing up memory space: Removal of vic input, output files and previous init_state_files older than 20 days.") cleaner.clean_vic() if(config['CLEAN_UP'].get('clean_routing')): - rat_logger.info("Clearing up memory space: Removal of routing input and output files") + rat_logger.info("Clearing up memory space: Removal of routing input, output files and previous rout_state_files older than 20 days.") cleaner.clean_routing() if(config['CLEAN_UP'].get('clean_gee')): rat_logger.info("Clearing up memory space: Removal of unwanted gee extracted small chunk files") diff --git a/src/rat/utils/clean.py b/src/rat/utils/clean.py index 0f3df9b8..659a7598 100644 --- a/src/rat/utils/clean.py +++ b/src/rat/utils/clean.py @@ -6,19 +6,32 @@ class Clean: def __init__(self, basin_data_dir): self.basin_data_dir = basin_data_dir + self.days_old_to_delete = 20 pass def clean_pre_processing(self): try: - pre_processing_path = os.path.join(self.basin_data_dir,'pre_processing','') - shutil.rmtree(pre_processing_path) + pre_processing_processed_path = os.path.join(self.basin_data_dir,'pre_processing','processed','') + shutil.rmtree(pre_processing_processed_path) except: - print("No pre_processing folder to delete") + print("No processed folder in pre_processing to delete") + + # try: + # pre_processing_nc_path = os.path.join(self.basin_data_dir,'pre_processing','nc','') + # shutil.rmtree(pre_processing_nc_path) + # except: + # print("No nc folder in pre_processing to delete") def clean_metsim(self): try: - metsim_path = os.path.join(self.basin_data_dir,'metsim','metsim_outputs','') - shutil.rmtree(metsim_path) + metsim_inputs_path = os.path.join(self.basin_data_dir,'metsim','metsim_inputs','') + shutil.rmtree(metsim_inputs_path) + except: + print("No metsim_inputs folder to delete") + + try: + metsim_outputs_path = os.path.join(self.basin_data_dir,'metsim','metsim_outputs','') + shutil.rmtree(metsim_outputs_path) except: print("No metsim_outputs folder to delete") @@ -37,7 +50,7 @@ def clean_vic(self): try: vic_init_states_dir_path = os.path.join(self.basin_data_dir,'vic','vic_init_states','') - days_old = 15 #n max of days + days_old = self.days_old_to_delete #n max of days time_interval = datetime.now() - timedelta(days_old) file_namelist = os.listdir(vic_init_states_dir_path) @@ -67,7 +80,7 @@ def clean_routing(self): try: rout_init_states_dir_path = os.path.join(self.basin_data_dir,'ro','rout_state_file','') - days_old = 15 #n max of days + days_old = self.days_old_to_delete #n max of days time_interval = datetime.now() - timedelta(days_old) file_namelist = os.listdir(rout_init_states_dir_path) From 12fa855d84a58f94e835ece36c4e70685162a198 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 8 Sep 2024 18:46:13 -0700 Subject: [PATCH 004/102] Added landsat 5 script for TMS-OS --- src/rat/core/sarea/TMS.py | 89 +++++- src/rat/core/sarea/sarea_cli_l5.py | 443 +++++++++++++++++++++++++++++ 2 files changed, 530 insertions(+), 2 deletions(-) create mode 100644 src/rat/core/sarea/sarea_cli_l5.py diff --git a/src/rat/core/sarea/TMS.py b/src/rat/core/sarea/TMS.py index 0fc98a0a..62035758 100644 --- a/src/rat/core/sarea/TMS.py +++ b/src/rat/core/sarea/TMS.py @@ -32,9 +32,11 @@ def __init__(self, reservoir_name, area=None, AREA_DEVIATION_THRESHOLD_PCNT=5): self.AREA_DEVIATION_THRESHOLD = self.area * AREA_DEVIATION_THRESHOLD_PCNT/100 def tms_os(self, + l5_dfpath: str = "", + l7_dfpath: str = "", l8_dfpath: str = "", + l9_dfpath: str = "", s2_dfpath: str = "", - l9_dfpath: str = "", s1_dfpath: str = "", CLOUD_THRESHOLD: float = 90.0, MIN_DATE: str = '2019-01-01' @@ -42,7 +44,10 @@ def tms_os(self, ## TODO: add conditional, S1 required, any one of optical datasets required """Implements the TMS-OS methodology Args: - l8_dfpath (string): Path of the surface area dataframe obtained using `sarea_cli_l8.py` - Landsat derived surface areas + l5_dfpath (string): Path of the surface area dataframe obtained using `sarea_cli_l5.py` - Landsat-5 derived surface areas + l7_dfpath (string): Path of the surface area dataframe obtained using `sarea_cli_l7.py` - Landsat-7 derived surface areas + l8_dfpath (string): Path of the surface area dataframe obtained using `sarea_cli_l8.py` - Landsat-8 derived surface areas + l9_dfpath (string): Path of the surface area dataframe obtained using `sarea_cli_l9.py` - Landsat-9 derived surface areas s2_dfpath (string): Path of the surface area dataframe obtained using `sarea_cli_s2.py` - Sentinel-2 derived surface areas s1_dfpath (string): Path of the surface area dataframe obtained using `sarea_cli_sar.py` - Sentinel-1 derived surface areas CLOUD_THRESHOLD (float): Threshold to use for cloud-masking in % (default: 90.0) @@ -51,11 +56,91 @@ def tms_os(self, MIN_DATE = pd.to_datetime(MIN_DATE, format='%Y-%m-%d') S2_TEMPORAL_RESOLUTION = 5 S1_TEMPORAL_RESOLUTION = 12 + L5_TEMPORAL_RESOLUTION = 16 + L7_TEMPORAL_RESOLUTION = 16 L8_TEMPORAL_RESOLUTION = 16 L9_TEMPORAL_RESOLUTION = 16 TO_MERGE = [] + if os.path.isfile(l5_dfpath): + # Read in Landsat-8 + l5df = pd.read_csv(l5_dfpath, parse_dates=['mosaic_enddate']).rename({ + 'mosaic_enddate': 'date', + 'water_area_cordeiro': 'water_area_uncorrected', + 'non_water_area_cordeiro': 'non_water_area', + 'corrected_area_cordeiro': 'water_area_corrected' + }, axis=1).set_index('date') + l5df = l5df[['water_area_uncorrected', 'non_water_area', 'cloud_area', 'water_area_corrected']] + l5df['cloud_percent'] = l5df['cloud_area']*100/(l5df['water_area_uncorrected']+l5df['non_water_area']+l5df['cloud_area']) + l5df.replace(-1, np.nan, inplace=True) + + # QUALITY_DESCRIPTION + # 0: Good, not interpolated either due to missing data or high clouds + # 1: Poor, interpolated either due to high clouds + # 2: Poor, interpolated either due to missing data + l5df.loc[:, "QUALITY_DESCRIPTION"] = 0 + l5df.loc[l5df['cloud_percent']>=CLOUD_THRESHOLD, ("water_area_uncorrected", "non_water_area", "water_area_corrected")] = np.nan + l5df.loc[l5df['cloud_percent']>=CLOUD_THRESHOLD, "QUALITY_DESCRIPTION"] = 1 + + # in some cases l5df may have duplicated rows (with same values) that have to be removed + if l5df.index.duplicated().sum() > 0: + print("Duplicated labels, deleting") + l5df = l5df[~l5df.index.duplicated(keep='last')] + + # Fill in the gaps in l5df created due to high cloud cover with np.nan values + l5df_interpolated = l5df.reindex(pd.date_range(l5df.index[0], l5df.index[-1], freq=f'{L5_TEMPORAL_RESOLUTION}D')) + l5df_interpolated.loc[np.isnan(l5df_interpolated["QUALITY_DESCRIPTION"]), "QUALITY_DESCRIPTION"] = 2 + l5df_interpolated.loc[np.isnan(l5df_interpolated['cloud_area']), 'cloud_area'] = max(l5df['cloud_area']) + l5df_interpolated.loc[np.isnan(l5df_interpolated['cloud_percent']), 'cloud_percent'] = 100 + l5df_interpolated.loc[np.isnan(l5df_interpolated['non_water_area']), 'non_water_area'] = 0 + l5df_interpolated.loc[np.isnan(l5df_interpolated['water_area_uncorrected']), 'water_area_uncorrected'] = 0 + + # Interpolate bad data + l5df_interpolated.loc[:, "water_area_corrected"] = l5df_interpolated.loc[:, "water_area_corrected"].interpolate(method="linear", limit_direction="forward") + l5df_interpolated['sat'] = 'l5' + + TO_MERGE.append(l5df_interpolated) + + if os.path.isfile(l7_dfpath): + # Read in Landsat-8 + l7df = pd.read_csv(l7_dfpath, parse_dates=['mosaic_enddate']).rename({ + 'mosaic_enddate': 'date', + 'water_area_cordeiro': 'water_area_uncorrected', + 'non_water_area_cordeiro': 'non_water_area', + 'corrected_area_cordeiro': 'water_area_corrected' + }, axis=1).set_index('date') + l7df = l7df[['water_area_uncorrected', 'non_water_area', 'cloud_area', 'water_area_corrected']] + l7df['cloud_percent'] = l7df['cloud_area']*100/(l7df['water_area_uncorrected']+l7df['non_water_area']+l7df['cloud_area']) + l7df.replace(-1, np.nan, inplace=True) + + # QUALITY_DESCRIPTION + # 0: Good, not interpolated either due to missing data or high clouds + # 1: Poor, interpolated either due to high clouds + # 2: Poor, interpolated either due to missing data + l7df.loc[:, "QUALITY_DESCRIPTION"] = 0 + l7df.loc[l7df['cloud_percent']>=CLOUD_THRESHOLD, ("water_area_uncorrected", "non_water_area", "water_area_corrected")] = np.nan + l7df.loc[l7df['cloud_percent']>=CLOUD_THRESHOLD, "QUALITY_DESCRIPTION"] = 1 + + # in some cases l7df may have duplicated rows (with same values) that have to be removed + if l7df.index.duplicated().sum() > 0: + print("Duplicated labels, deleting") + l7df = l7df[~l7df.index.duplicated(keep='last')] + + # Fill in the gaps in l7df created due to high cloud cover with np.nan values + l7df_interpolated = l7df.reindex(pd.date_range(l7df.index[0], l7df.index[-1], freq=f'{L7_TEMPORAL_RESOLUTION}D')) + l7df_interpolated.loc[np.isnan(l7df_interpolated["QUALITY_DESCRIPTION"]), "QUALITY_DESCRIPTION"] = 2 + l7df_interpolated.loc[np.isnan(l7df_interpolated['cloud_area']), 'cloud_area'] = max(l7df['cloud_area']) + l7df_interpolated.loc[np.isnan(l7df_interpolated['cloud_percent']), 'cloud_percent'] = 100 + l7df_interpolated.loc[np.isnan(l7df_interpolated['non_water_area']), 'non_water_area'] = 0 + l7df_interpolated.loc[np.isnan(l7df_interpolated['water_area_uncorrected']), 'water_area_uncorrected'] = 0 + + # Interpolate bad data + l7df_interpolated.loc[:, "water_area_corrected"] = l7df_interpolated.loc[:, "water_area_corrected"].interpolate(method="linear", limit_direction="forward") + l7df_interpolated['sat'] = 'l7' + + TO_MERGE.append(l7df_interpolated) + if os.path.isfile(l8_dfpath): # Read in Landsat-8 l8df = pd.read_csv(l8_dfpath, parse_dates=['mosaic_enddate']).rename({ diff --git a/src/rat/core/sarea/sarea_cli_l5.py b/src/rat/core/sarea/sarea_cli_l5.py new file mode 100644 index 00000000..1aa1ec5c --- /dev/null +++ b/src/rat/core/sarea/sarea_cli_l5.py @@ -0,0 +1,443 @@ +import ee +from datetime import datetime, timedelta, timezone +import pandas as pd +import time +import os +from random import randint +from itertools import zip_longest + +from rat.ee_utils.ee_utils import poly2feature +from rat.utils.logging import LOG_NAME, NOTIFICATION +from rat.utils.utils import days_between +from logging import getLogger + +# Defining global constants +l5 = ee.ImageCollection("LANDSAT/LT05/C02/T1_L2") +gswd = ee.Image("JRC/GSW1_3/GlobalSurfaceWater") + +NDWI_THRESHOLD = 0.3 +SMALL_SCALE = 30 +MEDIUM_SCALE = 120 +LARGE_SCALE = 500 +BUFFER_DIST = 500 +CLOUD_COVER_LIMIT = 90 +TEMPORAL_RESOLUTION = 16 +RESULTS_PER_ITER = 5 +QUALITY_PIXEL_BAND_NAME = 'QA_PIXEL' +BLUE_BAND_NAME = 'SR_B1' +GREEN_BAND_NAME = 'SR_B2' +RED_BAND_NAME = 'SR_B3' +NIR_BAND_NAME = 'SR_B4' +SWIR1_BAND_NAME = 'SR_B5' +SWIR2_BAND_NAME = 'SR_B7' + + +def preprocess(im): + clipped = im # clipping adds processing overhead, setting clipped = im + + # clipped = ee.Image(ee.Algorithms.If('BQA' in clipped.bandNames(), preprocess_image_mask(im), clipped.updateMask(ee.Image.constant(1)))) + # Mask appropriate QA bits + QA = im.select([QUALITY_PIXEL_BAND_NAME]) + + cloudShadowBitMask = 1 << 3 + cloudsBitMask = 1 << 4 + + cloud = (QA.bitwiseAnd(cloudsBitMask).neq(0)).Or(QA.bitwiseAnd(cloudShadowBitMask).neq(0)).rename("cloud") + + clipped = clipped.updateMask(cloud.eq(0).select("cloud")) + + # SR scaling + clipped = clipped.select('SR_B.').multiply(0.0000275).add(-0.2) + clipped = clipped.addBands(cloud) + + clipped = clipped.set('system:time_start', im.get('system:time_start')) + clipped = clipped.set('system:time_end', im.get('system:time_end')) + # clipped = clipped.set('cloud_area', cloud_area) + + return clipped + + +##########################################################/ +## Processing individual images - water classification ## +##########################################################/ + +def identify_water_cluster(im): + im = ee.Image(im) + mbwi = im.select('MBWI') + + max_cluster_value = ee.Number(im.select('cluster').reduceRegion( + reducer = ee.Reducer.max(), + geometry = aoi, + scale = LARGE_SCALE, + maxPixels = 1e10 + ).get('cluster')) + + clusters = ee.List.sequence(0, max_cluster_value) + + def calc_avg_mbwi(cluster_val): + cluster_val = ee.Number(cluster_val) + avg_mbwi = mbwi.updateMask(im.select('cluster').eq(ee.Image(cluster_val))).reduceRegion( + reducer = ee.Reducer.mean(), + scale = MEDIUM_SCALE, + geometry = aoi, + maxPixels = 1e10 + ).get('MBWI') + return avg_mbwi + + avg_mbwis = ee.Array(clusters.map(calc_avg_mbwi)) + + max_mbwi_index = avg_mbwis.argmax().get(0) + + water_cluster = clusters.get(max_mbwi_index) + + return water_cluster + + +def cordeiro(im): + band_subset = ee.List(['NDWI', 'MNDWI', SWIR2_BAND_NAME]) # using NDWI, MNDWI and B7 (SWIR2) + sampled_pts = im.select(band_subset).sample( + region = aoi, + scale = SMALL_SCALE, + numPixels = 5e3-1 ## limit of 5k points + ) + + ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on + ## calinski harabasz's work + ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans + clusterer = ee.Clusterer.wekaCascadeKMeans( + minClusters = 2, + maxClusters = 7, + init = True + ).train(sampled_pts) + + # Classify clusters + classified = im.select(band_subset).cluster(clusterer) + im = im.addBands(classified) + + # Select cluster with highest average MBWI and say it as water map cordeiro + water_cluster = identify_water_cluster(im) + water_map = classified.select('cluster').eq(ee.Image.constant(water_cluster)).select(['cluster'], ['water_map_cordeiro']) + im = im.addBands(water_map) + + return im + + +def process_image(im): + ndwi = im.normalizedDifference([NIR_BAND_NAME, SWIR1_BAND_NAME]).rename('NDWI') + im = im.addBands(ndwi) + mndwi = im.normalizedDifference([GREEN_BAND_NAME, SWIR1_BAND_NAME]).rename('MNDWI') + im = im.addBands(mndwi) + mbwi = im.expression("MBWI = 3*green-red-nir-swir1-swir2", { + 'green': im.select(GREEN_BAND_NAME), + 'red': im.select(RED_BAND_NAME), + 'nir': im.select(NIR_BAND_NAME), + 'swir1': im.select(SWIR1_BAND_NAME), + 'swir2': im.select(SWIR2_BAND_NAME) + }) + im = im.addBands(mbwi) + + #cloud_area = AOI area - area od pixels in cloud band where there is no data because we masked it coz of clouds + cloud_area = aoi.area().subtract(im.select('cloud').Not().multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('cloud')) + cloud_percent = cloud_area.multiply(100).divide(aoi.area()) + + cordeiro_will_run_when = cloud_percent.lt(CLOUD_COVER_LIMIT) + + # Clustering based classification of water pixels for cloud pixels when cloud cover is less than CLOUD_COVER_LIMIT + im = im.addBands(ee.Image(ee.Algorithms.If(cordeiro_will_run_when, cordeiro(im), ee.Image.constant(-1e6)))) # run cordeiro only if cloud percent is < 90% + + # Calculate water area in water_area_cordeiro map + water_area_cordeiro = ee.Number(ee.Algorithms.If(cordeiro_will_run_when, + ee.Number(im.select('water_map_cordeiro').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_cordeiro')), + ee.Number(-1e6) + )) + # Calculate non-water area in water_area_cordeiro map. + non_water_area_cordeiro = ee.Number(ee.Algorithms.If(cordeiro_will_run_when, + ee.Number(im.select('water_map_cordeiro').neq(1).multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_cordeiro')), + ee.Number(-1e6) + )) + + # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD + im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) + # Calculate water area in water_map_NDWI. + water_area_NDWI = ee.Number(im.select('water_map_NDWI').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate non-water area in water_map_NDWI. + non_water_area_NDWI = ee.Number(im.select('water_map_NDWI').neq(1).multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + + # Set attributes to retrive later + im = im.set('cloud_area', cloud_area.multiply(1e-6)) + im = im.set('cloud_percent', cloud_percent) + im = im.set('water_area_cordeiro', water_area_cordeiro.multiply(1e-6)) + im = im.set('non_water_area_cordeiro', non_water_area_cordeiro.multiply(1e-6)) + im = im.set('water_area_NDWI', water_area_NDWI.multiply(1e-6)) + im = im.set('non_water_area_NDWI', non_water_area_NDWI.multiply(1e-6)) + + return im + + +def postprocess_wrapper(im, bandName, raw_area): + im = ee.Image(im) + bandName = ee.String(bandName) + date = im.get('to_date') + + def postprocess(): + gswd_masked = gswd.updateMask(im.select(bandName).eq(1)) + + hist = ee.List(gswd_masked.reduceRegion( + reducer = ee.Reducer.autoHistogram(minBucketWidth = 1), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('occurrence')) + + counts = ee.Array(hist).transpose().toList() + + omega = ee.Number(0.17) + count_thresh = ee.Number(counts.map(lambda lis: ee.List(lis).reduce(ee.Reducer.mean())).get(1)).multiply(omega) + + count_thresh_index = ee.Array(counts.get(1)).gt(count_thresh).toList().indexOf(1) + occurrence_thresh = ee.Number(ee.List(counts.get(0)).get(count_thresh_index)) + + water_map = im.select([bandName], ['water_map']) + gswd_improvement = gswd.clip(aoi).gte(occurrence_thresh).updateMask(water_map.mask().Not()).select(["occurrence"], ["water_map"]) + + improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) + + corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_zhao_gao')) + + improved = improved.set("corrected_area", corrected_area.multiply(1e-6)) + return improved + + def dont_post_process(): + improved = ee.Image.constant(-1) + improved = improved.set("corrected_area", -1) + return improved + + condition = ee.Number(im.get('cloud_percent')).lt(CLOUD_COVER_LIMIT).And(ee.Number(raw_area).gt(0)) + improved = ee.Image(ee.Algorithms.If(condition, postprocess(), dont_post_process())) + + improved = improved.set("to_date", date) + + return improved + + +############################################################/ +## Code from here takes care of the time-series generation ## +############################################################/ +def calc_ndwi(im): + return im.addBands(im.normalizedDifference([NIR_BAND_NAME, SWIR1_BAND_NAME]).rename('NDWI')) + +def process_date(date): + # Given date, calculate end date by adding TEMPORAL_RESOLUTION - 1 days + date = ee.Date(date) + from_date = date + to_date = date.advance(TEMPORAL_RESOLUTION - 1, 'day') + # Filter the image collection for these dates and AOI and run preprocess function (cloud calculations, scaling, adding & setting start and end) on them + l5_subset = l5.filterDate(from_date, to_date).filterBounds(aoi).map(preprocess) + + # Get mosaic of images if there is atleast one image for this time duration with NDWI as the quality factor (keep High NDWI) + im = ee.Image(ee.Algorithms.If(l5_subset.size().neq(0), l5_subset.map(calc_ndwi).qualityMosaic('NDWI'), ee.Image.constant(0))) + # Process NDWI Image if there is atleast one image for this time duration + im = ee.Image(ee.Algorithms.If(l5_subset.size().neq(0), process_image(im), ee.Image.constant(0))) + + # Set attributes of from and to date along with number of images during the time duration + im = im.set('from_date', from_date.format("YYYY-MM-dd")) + im = im.set('to_date', to_date.format("YYYY-MM-dd")) + im = im.set('l5_images', l5_subset.size()) + + # im = ee.Algorithms.If(im.bandNames().size().eq(1), ee.Number(0), im) + + return ee.Image(im) + + +def generate_timeseries(dates): + raw_ts = dates.map(process_date) + # raw_ts = raw_ts.removeAll([0]) + imcoll = ee.ImageCollection.fromImages(raw_ts) + + return imcoll + +def get_first_obs(start_date, end_date): + first_im = l5.filterBounds(aoi).filterDate(start_date, end_date).first() + str_fmt = 'YYYY-MM-dd' + return ee.Date.parse(str_fmt, ee.Date(first_im.get('system:time_start')).format(str_fmt)) + +def run_process_long(res_name, res_polygon, start, end, datadir): + fo = start #fo: first observation + enddate = end + + # Extracting reservoir geometry + global aoi + aoi = poly2feature(res_polygon,BUFFER_DIST).geometry() + + ## Checking the number of images in the interval as Landsat 5 might have missing data for a lot of places for longer durations. + number_of_images = l5.filterBounds(aoi).filterDate(start, end).size().getInfo() + + if(number_of_images): + # getting first observation in the filtered collection + print('Checking first observation date in the given time interval.') + fo = get_first_obs(start, end).format('YYYY-MM-dd').getInfo() + first_obs = datetime.strptime(fo, '%Y-%m-%d') + print(f"First Observation: {first_obs}") + + scratchdir = os.path.join(datadir, "_scratch") + + # If data already exists, only get new data starting from the last one + savepath = os.path.join(datadir, f"{res_name}.csv") + + # If an existing file exists, + if os.path.isfile(savepath): + # Read the existing file + temp_df = pd.read_csv(savepath, parse_dates=['mosaic_enddate']).set_index('mosaic_enddate') + + # Get the last date in the existing file and adjust the first observation to before last date (last date might not be for this satellite. Its TMS-OS data's ;ast date.) + last_date = temp_df.index[-1].to_pydatetime() + fo = (last_date - timedelta(days=TEMPORAL_RESOLUTION*2)).strftime("%Y-%m-%d") + # Create an array with filepath + to_combine = [savepath] + print(f"Existing file found - Last observation ({TEMPORAL_RESOLUTION*2} day lag): {last_date}") + + # If {TEMPORAL_RESOLUTION} days have not passed since last observation, skip the processing + days_passed = (datetime.strptime(end, "%Y-%m-%d") - last_date).days + print(f"No. of days passed since: {days_passed}") + if days_passed < TEMPORAL_RESOLUTION: + print(f"No new observation expected. Quitting early") + return None + # If no file exists already, create an empty array + else: + to_combine = [] + + # Extracting data in scratch directory + savedir = os.path.join(scratchdir, f"{res_name}_l5_cordeiro_zhao_gao_{fo}_{enddate}") + if not os.path.isdir(savedir): + os.makedirs(savedir) + + print(f"Extracting SA for the period {fo} -> {enddate}") + + # Creating list of dates from fo to enddate with frequency of TEMPORAL_RESOLUTION + dates = pd.date_range(fo, enddate, freq=f'{TEMPORAL_RESOLUTION}D') + # Grouping dates into smaller arrays to process in GEE + grouped_dates = grouper(dates, RESULTS_PER_ITER) + + # For each smaller array of dates + for subset_dates in grouped_dates: + try: + print(subset_dates) + # Convert dates list to earth engine object + dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) + # Generate Timeseries of one image corresponding to each date with water area in its attributes + res = generate_timeseries(dates).filterMetadata('l5_images', 'greater_than', 0) + # Extracting uncorrected water area and other information from attributes + uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l5_images'] + uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') + uncorrected_final_data = uncorrected_final_data_ee.getInfo() + print("Uncorrected", uncorrected_final_data) + # Extracting corrected area after corrrecting for cloud covered pixels using zhao gao correction. + res_corrected_cordeiro = res.map(lambda im: postprocess_wrapper(im, 'water_map_cordeiro', im.get('water_area_cordeiro'))) + corrected_columns_to_extract = ['to_date', 'corrected_area'] + corrected_final_data_cordeiro_ee = res_corrected_cordeiro \ + .filterMetadata('corrected_area', 'not_equals', None) \ + .reduceColumns( + ee.Reducer.toList( + len(corrected_columns_to_extract)), + corrected_columns_to_extract + ).get('list') + corrected_final_data_cordeiro = corrected_final_data_cordeiro_ee.getInfo() + print("Corrected - Cordeiro", corrected_final_data_cordeiro) + # If no data point for this duration, then skip + if len(uncorrected_final_data) == 0: + continue + # Create pandas dataframes with the extracted information and merge them + uncorrected_df = pd.DataFrame(uncorrected_final_data, columns=uncorrected_columns_to_extract) + corrected_cordeiro_df = pd.DataFrame(corrected_final_data_cordeiro, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_cordeiro'}, axis=1) + df = pd.merge(uncorrected_df, corrected_cordeiro_df, 'left', 'to_date') + + df['from_date'] = pd.to_datetime(df['from_date'], format="%Y-%m-%d") + df['to_date'] = pd.to_datetime(df['to_date'], format="%Y-%m-%d") + df['mosaic_enddate'] = df['to_date'] - pd.Timedelta(1, unit='day') + df = df.set_index('mosaic_enddate') + print(df.head(2)) + # Save the dataframe on the disk + fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") + df.to_csv(fname) + # Create a randonm sleep time + s_time = randint(20, 30) + print(f"Sleeping for {s_time} seconds") + time.sleep(randint(20, 30)) + + if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: + print(f"Quitting: Reached enddate {enddate}") + break + elif df.index[-1].strftime('%Y-%m-%d') == fo: + print(f"Reached last available observation - {fo}") + break + except Exception as e: + log.error(e) + continue + + # Combine the files into one database + to_combine.extend([os.path.join(savedir, f) for f in os.listdir(savedir) if f.endswith(".csv")]) + if len(to_combine): + files = [pd.read_csv(f, parse_dates=["mosaic_enddate"]).set_index("mosaic_enddate") for f in to_combine] + + data = pd.concat(files).drop_duplicates().sort_values("mosaic_enddate") + data.to_csv(savepath) + + return savepath + else: + print("Observed data could not be processed to get surface area.") + return None + else: + print(f"No observation observed between {start} and {end}. Quitting!") + return None + +# User-facing wrapper function +def sarea_l5(res_name,res_polygon, start, end, datadir): + return run_process_long(res_name,res_polygon, start, end, datadir) + + + +def grouper(iterable, n, *, incomplete='fill', fillvalue=None): + "Collect data into non-overlapping fixed-length chunks or blocks" + # grouper('ABCDEFG', 3, fillvalue='x') --> ABC DEF Gxx + # grouper('ABCDEFG', 3, incomplete='strict') --> ABC DEF ValueError + # grouper('ABCDEFG', 3, incomplete='ignore') --> ABC DEF + args = [iter(iterable)] * n + if incomplete == 'fill': + return zip_longest(*args, fillvalue=fillvalue) + if incomplete == 'strict': + return zip(*args, strict=True) + if incomplete == 'ignore': + return zip(*args) + else: + raise ValueError('Expected fill, strict, or ignore') + From a55e6e8ea39978eb51c5e95501612cb0499ac03e Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 8 Sep 2024 18:59:17 -0700 Subject: [PATCH 005/102] Added landsat 7 script for tmsos. --- src/rat/core/sarea/sarea_cli_l7.py | 460 +++++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) create mode 100644 src/rat/core/sarea/sarea_cli_l7.py diff --git a/src/rat/core/sarea/sarea_cli_l7.py b/src/rat/core/sarea/sarea_cli_l7.py new file mode 100644 index 00000000..34422bcf --- /dev/null +++ b/src/rat/core/sarea/sarea_cli_l7.py @@ -0,0 +1,460 @@ +import ee +from datetime import datetime, timedelta, timezone +import pandas as pd +import time +import os +from random import randint +from itertools import zip_longest + +from rat.ee_utils.ee_utils import poly2feature +from rat.utils.logging import LOG_NAME, NOTIFICATION +from rat.utils.utils import days_between +from logging import getLogger + +# Defining global constants +l7= ee.ImageCollection("LANDSAT/LE07/C02/T1_L2") +gswd = ee.Image("JRC/GSW1_3/GlobalSurfaceWater") + +NDWI_THRESHOLD = 0.3 +SMALL_SCALE = 30 +MEDIUM_SCALE = 120 +LARGE_SCALE = 500 +BUFFER_DIST = 500 +CLOUD_COVER_LIMIT = 90 +TEMPORAL_RESOLUTION = 16 +RESULTS_PER_ITER = 5 +QUALITY_PIXEL_BAND_NAME = 'QA_PIXEL' +BLUE_BAND_NAME = 'SR_B1' +GREEN_BAND_NAME = 'SR_B2' +RED_BAND_NAME = 'SR_B3' +NIR_BAND_NAME = 'SR_B4' +SWIR1_BAND_NAME = 'SR_B5' +SWIR2_BAND_NAME = 'SR_B7' + +def preprocess(im): + clipped = im # clipping adds processing overhead, setting clipped = im + + # clipped = ee.Image(ee.Algorithms.If('BQA' in clipped.bandNames(), preprocess_image_mask(im), clipped.updateMask(ee.Image.constant(1)))) + # Mask appropriate QA bits + QA = im.select([QUALITY_PIXEL_BAND_NAME]) + + cloudShadowBitMask = 1 << 3 + cloudsBitMask = 1 << 4 + + cloud = (QA.bitwiseAnd(cloudsBitMask).neq(0)).Or(QA.bitwiseAnd(cloudShadowBitMask).neq(0)).rename("cloud") + clipped = clipped.updateMask(cloud.eq(0).select("cloud")) + + # SR scaling + clipped = clipped.select('SR_B.').multiply(0.0000275).add(-0.2) + clipped = clipped.addBands(cloud) + + clipped = clipped.set('system:time_start', im.get('system:time_start')) + clipped = clipped.set('system:time_end', im.get('system:time_end')) + # clipped = clipped.set('cloud_area', cloud_area) + + return clipped + + +##########################################################/ +## Processing individual images - water classification ## +##########################################################/ + +def identify_water_cluster(im): + im = ee.Image(im) + mbwi = im.select('MBWI') + + max_cluster_value = ee.Number(im.select('cluster').reduceRegion( + reducer = ee.Reducer.max(), + geometry = aoi, + scale = LARGE_SCALE, + maxPixels = 1e10 + ).get('cluster')) + + clusters = ee.List.sequence(0, max_cluster_value) + + def calc_avg_mbwi(cluster_val): + cluster_val = ee.Number(cluster_val) + avg_mbwi = mbwi.updateMask(im.select('cluster').eq(ee.Image(cluster_val))).reduceRegion( + reducer = ee.Reducer.mean(), + scale = MEDIUM_SCALE, + geometry = aoi, + maxPixels = 1e10 + ).get('MBWI') + return avg_mbwi + + avg_mbwis = ee.Array(clusters.map(calc_avg_mbwi)) + + max_mbwi_index = avg_mbwis.argmax().get(0) + + water_cluster = clusters.get(max_mbwi_index) + + return water_cluster + + +def cordeiro(im): + band_subset = ee.List(['NDWI', 'MNDWI', SWIR2_BAND_NAME]) # using NDWI, MNDWI and B7 (SWIR2) + sampled_pts = im.select(band_subset).sample( + region = aoi, + scale = SMALL_SCALE, + numPixels = 5e3-1 ## limit of 5k points + ) + + ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on + ## calinski harabasz's work + ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans + clusterer = ee.Clusterer.wekaCascadeKMeans( + minClusters = 2, + maxClusters = 7, + init = True + ).train(sampled_pts) + + # Classify clusters + classified = im.select(band_subset).cluster(clusterer) + im = im.addBands(classified) + + # Select cluster with highest average MBWI and say it as water map cordeiro + water_cluster = identify_water_cluster(im) + water_map = classified.select('cluster').eq(ee.Image.constant(water_cluster)).select(['cluster'], ['water_map_cordeiro']) + im = im.addBands(water_map) + + return im + + +def process_image(im): + # Landsat 7 process Scan Line Corrector (SLC) + + ndwi = im.normalizedDifference([NIR_BAND_NAME, SWIR1_BAND_NAME]).rename('NDWI') + im = im.addBands(ndwi) + mndwi = im.normalizedDifference([GREEN_BAND_NAME, SWIR1_BAND_NAME]).rename('MNDWI') + im = im.addBands(mndwi) + mbwi = im.expression("MBWI = 3*green-red-nir-swir1-swir2", { + 'green': im.select(GREEN_BAND_NAME), + 'red': im.select(RED_BAND_NAME), + 'nir': im.select(NIR_BAND_NAME), + 'swir1': im.select(SWIR1_BAND_NAME), + 'swir2': im.select(SWIR2_BAND_NAME) + }) + im = im.addBands(mbwi) + + #cloud_area = AOI area - area od pixels in cloud band where there is no data because we masked it due to presence of clouds + cloud_area = aoi.area().subtract(im.select('cloud').Not().multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('cloud')) + cloud_percent = cloud_area.multiply(100).divide(aoi.area()) + + cordeiro_will_run_when = cloud_percent.lt(CLOUD_COVER_LIMIT) + + # Clustering based classification of water pixels for cloud pixels when cloud cover is less than CLOUD_COVER_LIMIT + im = im.addBands(ee.Image(ee.Algorithms.If(cordeiro_will_run_when, cordeiro(im), ee.Image.constant(-1e6)))) # run cordeiro only if cloud percent is < 90% + + # Calculate water area in water_area_cordeiro map + water_area_cordeiro = ee.Number(ee.Algorithms.If(cordeiro_will_run_when, + ee.Number(im.select('water_map_cordeiro').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_cordeiro')), + ee.Number(-1e6) + )) + # Calculate non-water area in water_area_cordeiro map. + non_water_area_cordeiro = ee.Number(ee.Algorithms.If(cordeiro_will_run_when, + ee.Number(im.select('water_map_cordeiro').neq(1).multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_cordeiro')), + ee.Number(-1e6) + )) + + # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD + im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) + # Calculate water area in water_map_NDWI. + water_area_NDWI = ee.Number(im.select('water_map_NDWI').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate non-water area in water_map_NDWI. + non_water_area_NDWI = ee.Number(im.select('water_map_NDWI').neq(1).multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + + # Set attributes to retrive later + im = im.set('cloud_area', cloud_area.multiply(1e-6)) + im = im.set('cloud_percent', cloud_percent) + im = im.set('water_area_cordeiro', water_area_cordeiro.multiply(1e-6)) + im = im.set('non_water_area_cordeiro', non_water_area_cordeiro.multiply(1e-6)) + im = im.set('water_area_NDWI', water_area_NDWI.multiply(1e-6)) + im = im.set('non_water_area_NDWI', non_water_area_NDWI.multiply(1e-6)) + + return im + + +def postprocess_wrapper(im, bandName, raw_area): + im = ee.Image(im) + bandName = ee.String(bandName) + date = im.get('to_date') + + def postprocess(): + gswd_masked = gswd.updateMask(im.select(bandName).eq(1)) + + hist = ee.List(gswd_masked.reduceRegion( + reducer = ee.Reducer.autoHistogram(minBucketWidth = 1), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('occurrence')) + + counts = ee.Array(hist).transpose().toList() + + omega = ee.Number(0.17) + count_thresh = ee.Number(counts.map(lambda lis: ee.List(lis).reduce(ee.Reducer.mean())).get(1)).multiply(omega) + + count_thresh_index = ee.Array(counts.get(1)).gt(count_thresh).toList().indexOf(1) + occurrence_thresh = ee.Number(ee.List(counts.get(0)).get(count_thresh_index)) + + water_map = im.select([bandName], ['water_map']) + gswd_improvement = gswd.clip(aoi).gte(occurrence_thresh).updateMask(water_map.mask().Not()).select(["occurrence"], ["water_map"]) + + improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) + + corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_zhao_gao')) + + improved = improved.set("corrected_area", corrected_area.multiply(1e-6)) + return improved + + def dont_post_process(): + improved = ee.Image.constant(-1) + improved = improved.set("corrected_area", -1) + return improved + + condition = ee.Number(im.get('cloud_percent')).lt(CLOUD_COVER_LIMIT).And(ee.Number(raw_area).gt(0)) + improved = ee.Image(ee.Algorithms.If(condition, postprocess(), dont_post_process())) + + improved = improved.set("to_date", date) + + return improved + + +############################################################/ +## Code from here takes care of the time-series generation ## +############################################################/ +def calc_ndwi(im): + return im.addBands(im.normalizedDifference([NIR_BAND_NAME, SWIR1_BAND_NAME]).rename('NDWI')) + +def slc_failure_correction(im): + filled_image = im.select('SR_B.') + + # Apply focal mean with the radius of 2 + + focal_mean_image = filled_image.focal_mean(2, 'square', 'pixels', 2) + + # Create a mask for NaN areas in the filled image + nan_mask = filled_image.mask().Not() + + # Only update the NaN areas with focal mean result + filled_image = filled_image.blend(focal_mean_image.updateMask(nan_mask)) + + # Preserve the time metadata + filled_image = filled_image.set('system:time_start', im.get('system:time_start')) + filled_image = filled_image.set('system:time_end', im.get('system:time_end')) + + #Add the Quality pixel band + filled_image = filled_image.addBands(im.select(QUALITY_PIXEL_BAND_NAME)) + + return filled_image + + +def process_date(date): + # Given date, calculate end date by adding TEMPORAL_RESOLUTION - 1 days + date = ee.Date(date) + from_date = date + to_date = date.advance(TEMPORAL_RESOLUTION - 1, 'day') + + # from_date_client_obj = from_date.format('YYYY-MM-dd') + # from_date_client_obj = pd.to_datetime(from_date_client_obj) + + # Only do SLC failure correction if date is greater than 31st may 2003. + # if(from_date_client_obj>=pd.to_datetime('2003-05-31')): + # Filter the image collection for these dates and AOI and do SLC failure correction for landsat-7 and run preprocess function (cloud calculations, scaling, adding & setting start and end) on them + l7_subset = ee.ImageCollection(ee.Algorithms.If(ee.Date('2003-05-31').millis().lt(to_date.millis()), + l7.filterDate(from_date, to_date).filterBounds(aoi).map(slc_failure_correction).map(preprocess), + l7.filterDate(from_date, to_date).filterBounds(aoi).map(preprocess))) + # else: + # # Filter the image collection for these dates and AOI and run preprocess function (cloud calculations, scaling, adding & setting start and end) on them + # l7_subset = l7.filterDate(from_date, to_date).filterBounds(aoi).map(preprocess) + + # Get mosaic of images if there is atleast one image for this time duration with NDWI as the quality factor (keep High NDWI) + im = ee.Image(ee.Algorithms.If(l7_subset.size().neq(0), l7_subset.map(calc_ndwi).qualityMosaic('NDWI'), ee.Image.constant(0))) + # Process NDWI Image if there is atleast one image for this time duration + im = ee.Image(ee.Algorithms.If(l7_subset.size().neq(0), process_image(im), ee.Image.constant(0))) + + # Set attributes of from and to date along with number of images during the time duration + im = im.set('from_date', from_date.format("YYYY-MM-dd")) + im = im.set('to_date', to_date.format("YYYY-MM-dd")) + im = im.set('l7_images', l7_subset.size()) + + # im = ee.Algorithms.If(im.bandNames().size().eq(1), ee.Number(0), im) + + return ee.Image(im) + + +def generate_timeseries(dates): + raw_ts = dates.map(process_date) + # raw_ts = raw_ts.removeAll([0]) + imcoll = ee.ImageCollection.fromImages(raw_ts) + + return imcoll + +def get_first_obs(start_date, end_date): + first_im = l7.filterBounds(aoi).filterDate(start_date, end_date).first() + str_fmt = 'YYYY-MM-dd' + return ee.Date.parse(str_fmt, ee.Date(first_im.get('system:time_start')).format(str_fmt)) + +def run_process_long(res_name, res_polygon, start, end, datadir): + fo = start #fo: first observation + enddate = end + + # Extracting reservoir geometry + global aoi + aoi = poly2feature(res_polygon,BUFFER_DIST).geometry() + + ## Checking the number of images in the interval as Landsat 7 might have missing data for a lot of places for longer durations. + number_of_images = l7.filterBounds(aoi).filterDate(start, end).size().getInfo() + + if(number_of_images): + # getting first observation in the filtered collection + print('Checking first observation date in the given time interval.') + fo = get_first_obs(start, end).format('YYYY-MM-dd').getInfo() + first_obs = datetime.strptime(fo, '%Y-%m-%d') + print(f"First Observation: {first_obs}") + + scratchdir = os.path.join(datadir, "_scratch") + + # If data already exists, only get new data starting from the last one + savepath = os.path.join(datadir, f"{res_name}.csv") + + # If an existing file exists, + if os.path.isfile(savepath): + # Read the existing file + temp_df = pd.read_csv(savepath, parse_dates=['mosaic_enddate']).set_index('mosaic_enddate') + + # Get the last date in the existing file and adjust the first observation to before last date (last date might not be for this satellite. Its TMS-OS data's ;ast date.) + last_date = temp_df.index[-1].to_pydatetime() + fo = (last_date - timedelta(days=TEMPORAL_RESOLUTION*2)).strftime("%Y-%m-%d") + # Create an array with filepath + to_combine = [savepath] + print(f"Existing file found - Last observation ({TEMPORAL_RESOLUTION*2} day lag): {last_date}") + + # If {TEMPORAL_RESOLUTION} days have not passed since last observation, skip the processing + days_passed = (datetime.strptime(end, "%Y-%m-%d") - last_date).days + print(f"No. of days passed since: {days_passed}") + if days_passed < TEMPORAL_RESOLUTION: + print(f"No new observation expected. Quitting early") + return None + # If no file exists already, create an empty array + else: + to_combine = [] + + # Extracting data in scratch directory + savedir = os.path.join(scratchdir, f"{res_name}_l7_cordeiro_zhao_gao_{fo}_{enddate}") + if not os.path.isdir(savedir): + os.makedirs(savedir) + + print(f"Extracting SA for the period {fo} -> {enddate}") + + # Creating list of dates from fo to enddate with frequency of TEMPORAL_RESOLUTION + dates = pd.date_range(fo, enddate, freq=f'{TEMPORAL_RESOLUTION}D') + # Grouping dates into smaller arrays to process in GEE + grouped_dates = grouper(dates, RESULTS_PER_ITER) + + # For each smaller array of dates + for subset_dates in grouped_dates: + try: + print(subset_dates) + # Convert dates list to earth engine object + dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) + # Generate Timeseries of one image corresponding to each date with water area in its attributes + res = generate_timeseries(dates).filterMetadata('l7_images', 'greater_than', 0) + # Extracting uncorrected water area and other information from attributes + uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l7_images'] + uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') + uncorrected_final_data = uncorrected_final_data_ee.getInfo() + print("Uncorrected", uncorrected_final_data) + # Extracting corrected area after corrrecting for cloud covered pixels using zhao gao correction. + res_corrected_cordeiro = res.map(lambda im: postprocess_wrapper(im, 'water_map_cordeiro', im.get('water_area_cordeiro'))) + corrected_columns_to_extract = ['to_date', 'corrected_area'] + corrected_final_data_cordeiro_ee = res_corrected_cordeiro \ + .filterMetadata('corrected_area', 'not_equals', None) \ + .reduceColumns( + ee.Reducer.toList( + len(corrected_columns_to_extract)), + corrected_columns_to_extract + ).get('list') + corrected_final_data_cordeiro = corrected_final_data_cordeiro_ee.getInfo() + print("Corrected - Cordeiro", corrected_final_data_cordeiro) + # If no data point for this duration, then skip + if len(uncorrected_final_data) == 0: + continue + # Create pandas dataframes with the extracted information and merge them + uncorrected_df = pd.DataFrame(uncorrected_final_data, columns=uncorrected_columns_to_extract) + corrected_cordeiro_df = pd.DataFrame(corrected_final_data_cordeiro, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_cordeiro'}, axis=1) + df = pd.merge(uncorrected_df, corrected_cordeiro_df, 'left', 'to_date') + + df['from_date'] = pd.to_datetime(df['from_date'], format="%Y-%m-%d") + df['to_date'] = pd.to_datetime(df['to_date'], format="%Y-%m-%d") + df['mosaic_enddate'] = df['to_date'] - pd.Timedelta(1, unit='day') + df = df.set_index('mosaic_enddate') + print(df.head(2)) + # Save the dataframe on the disk + fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") + df.to_csv(fname) + # Create a randonm sleep time + s_time = randint(20, 30) + print(f"Sleeping for {s_time} seconds") + time.sleep(randint(20, 30)) + + if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: + print(f"Quitting: Reached enddate {enddate}") + break + elif df.index[-1].strftime('%Y-%m-%d') == fo: + print(f"Reached last available observation - {fo}") + break + except Exception as e: + print(e) + # log.error(e) + continue + + # Combine the files into one database + to_combine.extend([os.path.join(savedir, f) for f in os.listdir(savedir) if f.endswith(".csv")]) + if len(to_combine): + files = [pd.read_csv(f, parse_dates=["mosaic_enddate"]).set_index("mosaic_enddate") for f in to_combine] + + data = pd.concat(files).drop_duplicates().sort_values("mosaic_enddate") + data.to_csv(savepath) + + return savepath + else: + print("Observed data could not be processed to get surface area.") + return None + else: + print(f"No observation observed between {start} and {end}. Quitting!") + return None + +# User-facing wrapper function +def sarea_l7(res_name,res_polygon, start, end, datadir): + return run_process_long(res_name,res_polygon, start, end, datadir) From 0c8b715e3236e21110b6d1443eebe2200490ec0c Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 8 Sep 2024 18:59:57 -0700 Subject: [PATCH 006/102] Used landsat 5 and 7 script for TMS-OS in sarea fn --- src/rat/core/run_sarea.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/rat/core/run_sarea.py b/src/rat/core/run_sarea.py index 000b79d6..19ee230b 100644 --- a/src/rat/core/run_sarea.py +++ b/src/rat/core/run_sarea.py @@ -5,6 +5,8 @@ from rat.utils.logging import LOG_NAME, NOTIFICATION, LOG_LEVEL1_NAME from rat.core.sarea.sarea_cli_s2 import sarea_s2 +from rat.core.sarea.sarea_cli_l5 import sarea_l5 +from rat.core.sarea.sarea_cli_l7 import sarea_l7 from rat.core.sarea.sarea_cli_l8 import sarea_l8 from rat.core.sarea.sarea_cli_l9 import sarea_l9 from rat.core.sarea.sarea_cli_sar import sarea_s1 @@ -70,6 +72,16 @@ def run_sarea_for_res(reservoir_name, reservoir_area, reservoir_polygon, start_d sarea_s2(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(datadir, 's2')) s2_dfpath = os.path.join(datadir, 's2', reservoir_name+'.csv') + # Landsat-5 + log.debug(f"Reservoir: {reservoir_name}; Downloading Landsat-5 data from {start_date} to {end_date}") + sarea_l5(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(datadir, 'l5')) + l5_dfpath = os.path.join(datadir, 'l5', reservoir_name+'.csv') + + # Landsat-7 + log.debug(f"Reservoir: {reservoir_name}; Downloading Landsat-7 data from {start_date} to {end_date}") + sarea_l7(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(datadir, 'l7')) + l7_dfpath = os.path.join(datadir, 'l7', reservoir_name+'.csv') + # Landsat-8 log.debug(f"Reservoir: {reservoir_name}; Downloading Landsat-8 data from {start_date} to {end_date}") sarea_l8(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(datadir, 'l8')) @@ -86,7 +98,8 @@ def run_sarea_for_res(reservoir_name, reservoir_area, reservoir_polygon, start_d s1_dfpath = os.path.join(datadir, 'sar', reservoir_name+'_12d_sar.csv') tmsos = TMS(reservoir_name, reservoir_area) - result,method = tmsos.tms_os(l9_dfpath=l9_dfpath, l8_dfpath=l8_dfpath, s2_dfpath=s2_dfpath, s1_dfpath=s1_dfpath) + result,method = tmsos.tms_os(l5_dfpath=l5_dfpath, l7_dfpath=l7_dfpath, l9_dfpath=l9_dfpath, l8_dfpath=l8_dfpath, + s2_dfpath=s2_dfpath, s1_dfpath=s1_dfpath) tmsos_savepath = os.path.join(datadir, reservoir_name+'.csv') log.debug(f"Saving surface area of {reservoir_name} at {tmsos_savepath}") From 9b74736eca79e4a58324b359e780198a4e94a7ca Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 3 Nov 2024 20:29:22 -0800 Subject: [PATCH 007/102] added ssc component calculation in landsat7 --- src/rat/core/sarea/sarea_cli_l7.py | 31 +++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_l7.py b/src/rat/core/sarea/sarea_cli_l7.py index 34422bcf..c85a1fdb 100644 --- a/src/rat/core/sarea/sarea_cli_l7.py +++ b/src/rat/core/sarea/sarea_cli_l7.py @@ -13,7 +13,7 @@ # Defining global constants l7= ee.ImageCollection("LANDSAT/LE07/C02/T1_L2") -gswd = ee.Image("JRC/GSW1_3/GlobalSurfaceWater") +gswd = ee.Image("JRC/GSW1_4/GlobalSurfaceWater") NDWI_THRESHOLD = 0.3 SMALL_SCALE = 30 @@ -171,7 +171,7 @@ def process_image(im): ee.Number(-1e6) )) - # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD + # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) # Calculate water area in water_map_NDWI. water_area_NDWI = ee.Number(im.select('water_map_NDWI').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( @@ -187,6 +187,27 @@ def process_image(im): scale = SMALL_SCALE, maxPixels = 1e10 ).get('water_map_NDWI')) + # Calculate red band sum for water area in water_map_NDWI. + water_red_sum = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(RED_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate green band sum for water area in water_map_NDWI. + water_green_sum = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(GREEN_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate nir band sum for water area in water_map_NDWI. + water_nir_sum = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(NIR_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) # Set attributes to retrive later im = im.set('cloud_area', cloud_area.multiply(1e-6)) @@ -195,6 +216,9 @@ def process_image(im): im = im.set('non_water_area_cordeiro', non_water_area_cordeiro.multiply(1e-6)) im = im.set('water_area_NDWI', water_area_NDWI.multiply(1e-6)) im = im.set('non_water_area_NDWI', non_water_area_NDWI.multiply(1e-6)) + im = im.set('water_red_sum', water_red_sum) + im = im.set('water_green_sum', water_green_sum) + im = im.set('water_nir_sum', water_nir_sum) return im @@ -391,7 +415,8 @@ def run_process_long(res_name, res_polygon, start, end, datadir): # Generate Timeseries of one image corresponding to each date with water area in its attributes res = generate_timeseries(dates).filterMetadata('l7_images', 'greater_than', 0) # Extracting uncorrected water area and other information from attributes - uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l7_images'] + uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l7_images', + 'water_red_sum', 'water_green_sum', 'water_nir_sum'] uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') uncorrected_final_data = uncorrected_final_data_ee.getInfo() print("Uncorrected", uncorrected_final_data) From 49ba4799ffa2862aed34449e58a76c77c0b5cc2a Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 3 Nov 2024 20:33:15 -0800 Subject: [PATCH 008/102] added ssc component calculation in landsat5 --- src/rat/core/sarea/sarea_cli_l5.py | 31 +++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_l5.py b/src/rat/core/sarea/sarea_cli_l5.py index 1aa1ec5c..5588854e 100644 --- a/src/rat/core/sarea/sarea_cli_l5.py +++ b/src/rat/core/sarea/sarea_cli_l5.py @@ -13,7 +13,7 @@ # Defining global constants l5 = ee.ImageCollection("LANDSAT/LT05/C02/T1_L2") -gswd = ee.Image("JRC/GSW1_3/GlobalSurfaceWater") +gswd = ee.Image("JRC/GSW1_4/GlobalSurfaceWater") NDWI_THRESHOLD = 0.3 SMALL_SCALE = 30 @@ -171,7 +171,7 @@ def process_image(im): ee.Number(-1e6) )) - # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD + # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) # Calculate water area in water_map_NDWI. water_area_NDWI = ee.Number(im.select('water_map_NDWI').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( @@ -187,6 +187,27 @@ def process_image(im): scale = SMALL_SCALE, maxPixels = 1e10 ).get('water_map_NDWI')) + # Calculate red band sum for water area in water_map_NDWI. + water_red_sum = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(RED_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate green band sum for water area in water_map_NDWI. + water_green_sum = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(GREEN_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate nir band sum for water area in water_map_NDWI. + water_nir_sum = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(NIR_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) # Set attributes to retrive later im = im.set('cloud_area', cloud_area.multiply(1e-6)) @@ -195,6 +216,9 @@ def process_image(im): im = im.set('non_water_area_cordeiro', non_water_area_cordeiro.multiply(1e-6)) im = im.set('water_area_NDWI', water_area_NDWI.multiply(1e-6)) im = im.set('non_water_area_NDWI', non_water_area_NDWI.multiply(1e-6)) + im = im.set('water_red_sum', water_red_sum) + im = im.set('water_green_sum', water_green_sum) + im = im.set('water_nir_sum', water_nir_sum) return im @@ -357,7 +381,8 @@ def run_process_long(res_name, res_polygon, start, end, datadir): # Generate Timeseries of one image corresponding to each date with water area in its attributes res = generate_timeseries(dates).filterMetadata('l5_images', 'greater_than', 0) # Extracting uncorrected water area and other information from attributes - uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l5_images'] + uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l5_images', + 'water_red_sum', 'water_green_sum', 'water_nir_sum'] uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') uncorrected_final_data = uncorrected_final_data_ee.getInfo() print("Uncorrected", uncorrected_final_data) From b52362d3baf24356c7a1ff0a2e4c4e8958b7a21e Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 3 Nov 2024 20:42:19 -0800 Subject: [PATCH 009/102] added ssc component calculation in landsat8 --- src/rat/core/sarea/sarea_cli_l8.py | 38 +++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_l8.py b/src/rat/core/sarea/sarea_cli_l8.py index 84fb958e..d1c1164b 100644 --- a/src/rat/core/sarea/sarea_cli_l8.py +++ b/src/rat/core/sarea/sarea_cli_l8.py @@ -36,7 +36,7 @@ def grouper(iterable, n, *, incomplete='fill', fillvalue=None): # NEW STUFF l8 = ee.ImageCollection("LANDSAT/LC08/C02/T1_L2") -gswd = ee.Image("JRC/GSW1_3/GlobalSurfaceWater") +gswd = ee.Image("JRC/GSW1_4/GlobalSurfaceWater") rgb_vis_params = {"bands":["B4","B3","B2"],"min":0,"max":0.4} NDWI_THRESHOLD = 0.3 @@ -49,6 +49,13 @@ def grouper(iterable, n, *, incomplete='fill', fillvalue=None): end_date = ee.Date('2019-02-01') TEMPORAL_RESOLUTION = 16 RESULTS_PER_ITER = 5 +QUALITY_PIXEL_BAND_NAME = 'QA_PIXEL' +BLUE_BAND_NAME = 'SR_B2' +GREEN_BAND_NAME = 'SR_B3' +RED_BAND_NAME = 'SR_B4' +NIR_BAND_NAME = 'SR_B5' +SWIR1_BAND_NAME = 'SR_B6' +SWIR2_BAND_NAME = 'SR_B7' # s2_subset = s2.filterBounds(aoi).filterDate(start_date, end_date) @@ -198,7 +205,7 @@ def process_image(im): ee.Number(-1e6) )) - # NDWI based + # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) water_area_NDWI = ee.Number(im.select('water_map_NDWI').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), @@ -212,6 +219,27 @@ def process_image(im): scale = SMALL_SCALE, maxPixels = 1e10 ).get('water_map_NDWI')) + # Calculate red band sum for water area in water_map_NDWI. + water_red_sum = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(RED_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate green band sum for water area in water_map_NDWI. + water_green_sum = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(GREEN_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate nir band sum for water area in water_map_NDWI. + water_nir_sum = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(NIR_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) im = im.set('cloud_area', cloud_area.multiply(1e-6)) im = im.set('cloud_percent', cloud_percent) @@ -219,6 +247,9 @@ def process_image(im): im = im.set('non_water_area_cordeiro', non_water_area_cordeiro.multiply(1e-6)) im = im.set('water_area_NDWI', water_area_NDWI.multiply(1e-6)) im = im.set('non_water_area_NDWI', non_water_area_NDWI.multiply(1e-6)) + im = im.set('water_red_sum', water_red_sum) + im = im.set('water_green_sum', water_green_sum) + im = im.set('water_nir_sum', water_nir_sum) return im @@ -381,7 +412,8 @@ def run_process_long(res_name, res_polygon, start, end, datadir): res = generate_timeseries(dates).filterMetadata('l8_images', 'greater_than', 0) # pprint.pprint(res.getInfo()) - uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l8_images'] + uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l8_images', + 'water_red_sum', 'water_green_sum', 'water_nir_sum'] uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') uncorrected_final_data = uncorrected_final_data_ee.getInfo() print("Uncorrected", uncorrected_final_data) From f4977cf761367b69ad57476f9070b6e1a89f0f97 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 3 Nov 2024 20:48:10 -0800 Subject: [PATCH 010/102] added ssc component calculation in landsat9 --- src/rat/core/sarea/sarea_cli_l9.py | 38 +++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_l9.py b/src/rat/core/sarea/sarea_cli_l9.py index f5702bb9..dc6b57cc 100644 --- a/src/rat/core/sarea/sarea_cli_l9.py +++ b/src/rat/core/sarea/sarea_cli_l9.py @@ -38,7 +38,7 @@ def grouper(iterable, n, *, incomplete='fill', fillvalue=None): # NEW STUFF l9 = ee.ImageCollection("LANDSAT/LC09/C02/T1_L2") -gswd = ee.Image("JRC/GSW1_3/GlobalSurfaceWater") +gswd = ee.Image("JRC/GSW1_4/GlobalSurfaceWater") rgb_vis_params = {"bands":["B4","B3","B2"],"min":0,"max":0.4} NDWI_THRESHOLD = 0.3 @@ -52,6 +52,13 @@ def grouper(iterable, n, *, incomplete='fill', fillvalue=None): TEMPORAL_RESOLUTION = 16 RESULTS_PER_ITER = 5 MISSION_START_DATE = (2022,1,1) # Rough start date for mission/satellite data +QUALITY_PIXEL_BAND_NAME = 'QA_PIXEL' +BLUE_BAND_NAME = 'SR_B2' +GREEN_BAND_NAME = 'SR_B3' +RED_BAND_NAME = 'SR_B4' +NIR_BAND_NAME = 'SR_B5' +SWIR1_BAND_NAME = 'SR_B6' +SWIR2_BAND_NAME = 'SR_B7' # s2_subset = s2.filterBounds(aoi).filterDate(start_date, end_date) @@ -201,7 +208,7 @@ def process_image(im): ee.Number(-1e6) )) - # NDWI based + # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) water_area_NDWI = ee.Number(im.select('water_map_NDWI').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), @@ -215,6 +222,27 @@ def process_image(im): scale = SMALL_SCALE, maxPixels = 1e10 ).get('water_map_NDWI')) + # Calculate red band sum for water area in water_map_NDWI. + water_red_sum = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(RED_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate green band sum for water area in water_map_NDWI. + water_green_sum = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(GREEN_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate nir band sum for water area in water_map_NDWI. + water_nir_sum = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(NIR_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) im = im.set('cloud_area', cloud_area.multiply(1e-6)) im = im.set('cloud_percent', cloud_percent) @@ -222,6 +250,9 @@ def process_image(im): im = im.set('non_water_area_cordeiro', non_water_area_cordeiro.multiply(1e-6)) im = im.set('water_area_NDWI', water_area_NDWI.multiply(1e-6)) im = im.set('non_water_area_NDWI', non_water_area_NDWI.multiply(1e-6)) + im = im.set('water_red_sum', water_red_sum) + im = im.set('water_green_sum', water_green_sum) + im = im.set('water_nir_sum', water_nir_sum) return im @@ -388,7 +419,8 @@ def run_process_long(res_name, res_polygon, start, end, datadir): res = generate_timeseries(dates).filterMetadata('l9_images', 'greater_than', 0) # pprint.pprint(res.getInfo()) - uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l9_images'] + uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l9_images', + 'water_red_sum', 'water_green_sum', 'water_nir_sum'] uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') uncorrected_final_data = uncorrected_final_data_ee.getInfo() print("Uncorrected", uncorrected_final_data) From df7218cd57f52a389ab1fd20c7e75ea06ebdc1ae Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 3 Nov 2024 21:33:17 -0800 Subject: [PATCH 011/102] added ssc component calculation in sentinel 2 --- src/rat/core/sarea/sarea_cli_s2.py | 104 ++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 05858592..8039e813 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -33,7 +33,7 @@ def grouper(iterable, n, *, incomplete='fill', fillvalue=None): # NEW STUFF s2 = ee.ImageCollection("COPERNICUS/S2_SR") -gswd = ee.Image("JRC/GSW1_3/GlobalSurfaceWater") +gswd = ee.Image("JRC/GSW1_4/GlobalSurfaceWater") rgb_vis_params = {"bands":["B4","B3","B2"],"min":0,"max":0.4} NDWI_THRESHOLD = 0.3; @@ -46,6 +46,14 @@ def grouper(iterable, n, *, incomplete='fill', fillvalue=None): end_date = ee.Date('2019-02-01') TEMPORAL_RESOLUTION = 5 RESULTS_PER_ITER = 5 +MISSION_START_DATE = (2022,1,1) # Rough start date for mission/satellite data +QUALITY_PIXEL_BAND_NAME = 'QA_PIXEL' +BLUE_BAND_NAME = 'B2' +GREEN_BAND_NAME = 'B3' +RED_BAND_NAME = 'B4' +NIR_BAND_NAME = 'B8' +SWIR1_BAND_NAME = 'B11' +SWIR2_BAND_NAME = 'B12' # aoi = reservoir.geometry().simplify(100).buffer(500); @@ -206,12 +214,84 @@ def process_image(im): ee.Number(-1e6) ) ) + # Calculate red band sum for water area in water_map_clustering. + water_red_sum = ee.Number( + ee.Algorithms.If( + CLOUD_LIMIT_SATISFIED, + ee.Number(im.select('water_map_clustering').eq(1).multiply(im.select(RED_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_clustering')), + ee.Number(-1e6) + ) + ) + # Calculate green band sum for water area in water_map_NDWI. + water_green_sum = ee.Number( + ee.Algorithms.If( + CLOUD_LIMIT_SATISFIED, + ee.Number(im.select('water_map_clustering').eq(1).multiply(im.select(GREEN_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_clustering')), + ee.Number(-1e6) + ) + ) + # Calculate nir band sum for water area in water_map_NDWI. + water_nir_sum = ee.Number( + ee.Algorithms.If( + CLOUD_LIMIT_SATISFIED, + ee.Number(im.select('water_map_clustering').eq(1).multiply(im.select(NIR_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_clustering')), + ee.Number(-1e6) + ) + ) + # Calculate red band/green band mean for water area in water_map_NDWI. + water_red_green_mean = ee.Number( + ee.Algorithms.If( + CLOUD_LIMIT_SATISFIED, + ee.Number(im.select('water_map_clustering').eq(1).multiply(im.select(RED_BAND_NAME)).divide(im.select( + GREEN_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.mean(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_clustering')), + ee.Number(-1e6) + ) + ) + # Calculate nir band/red band mean for water area in water_map_NDWI. + water_nir_red_mean = ee.Number( + ee.Algorithms.If( + CLOUD_LIMIT_SATISFIED, + ee.Number(im.select('water_map_clustering').eq(1).multiply(im.select(NIR_BAND_NAME)).divide(im.select( + RED_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.mean(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_clustering')), + ee.Number(-1e6) + ) + ) im = im.set('cloud_area', cloud_area.multiply(1e-6)) im = im.set('cloud_percent', cloud_percent) im = im.set('water_area_clustering', water_area_clustering.multiply(1e-6)) im = im.set('non_water_area_clustering', non_water_area_clustering.multiply(1e-6)) im = im.set('PROCESSING_SUCCESSFUL', CLOUD_LIMIT_SATISFIED) + im = im.set('water_red_sum', water_red_sum) + im = im.set('water_green_sum', water_green_sum) + im = im.set('water_nir_sum', water_nir_sum) + im = im.set('water_red_green_mean', water_red_green_mean) + im = im.set('water_nir_red_mean', water_nir_red_mean) return im @@ -410,6 +490,11 @@ def run_process_long(res_name,res_polygon, start, end, datadir): non_water_areas = [] water_areas = [] water_areas_zhaogao = [] + water_red_sums = [] + water_green_sums = [] + water_nir_sums = [] + water_red_green_means = [] + water_nir_red_means = [] for f, f_postprocessed in zip(ts_imcoll_L['features'], postprocessed_ts_imcoll_L['features']): PROCESSING_STATUS = f['properties']['PROCESSING_SUCCESSFUL'] PROCESSING_STATUSES.append(PROCESSING_STATUS) @@ -423,11 +508,21 @@ def run_process_long(res_name,res_polygon, start, end, datadir): non_water_areas.append(f['properties']['non_water_area_clustering']) cloud_areas.append(f['properties']['cloud_area']) cloud_percents.append(f['properties']['cloud_percent']) + water_red_sums.append(f['properties']['water_red_sum']) + water_green_sums.append(f['properties']['water_green_sum']) + water_nir_sums.append(f['properties']['water_nir_sum']) + water_red_green_means.append(f['properties']['water_red_green_mean']) + water_nir_red_means.append(f['properties']['water_nir_red_mean']) else: water_areas.append(np.nan) non_water_areas.append(np.nan) cloud_areas.append(np.nan) cloud_percents.append(np.nan) + water_red_sums.append(np.nan) + water_green_sums.append(np.nan) + water_nir_sums.append(np.nan) + water_red_green_means.append(np.nan) + water_nir_red_means.append(np.nan) if POSTPROCESSING_STATUS: water_areas_zhaogao.append(f_postprocessed['properties']['corrected_area']) else: @@ -443,7 +538,12 @@ def run_process_long(res_name,res_polygon, start, end, datadir): 'cloud_percent': cloud_percents, 'water_area_uncorrected': water_areas, 'non_water_area': non_water_areas, - 'water_area_corrected': water_areas_zhaogao + 'water_area_corrected': water_areas_zhaogao, + 'water_red_sum': water_red_sums, + 'water_green_sum': water_green_sums, + 'water_nir_sum': water_nir_sums, + 'water_red_green_mean': water_red_green_means, + 'water_nir_red_mean': water_nir_red_means }).set_index('date') fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") From 6385ad771975603ff73ca4211e72591f9d37d0d6 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 3 Nov 2024 21:33:37 -0800 Subject: [PATCH 012/102] Added pixel by pixel ratios for SSC as well --- src/rat/core/sarea/sarea_cli_l5.py | 20 +++++++++++++++++++- src/rat/core/sarea/sarea_cli_l7.py | 20 +++++++++++++++++++- src/rat/core/sarea/sarea_cli_l8.py | 20 +++++++++++++++++++- src/rat/core/sarea/sarea_cli_l9.py | 20 +++++++++++++++++++- 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_l5.py b/src/rat/core/sarea/sarea_cli_l5.py index 5588854e..aaad3275 100644 --- a/src/rat/core/sarea/sarea_cli_l5.py +++ b/src/rat/core/sarea/sarea_cli_l5.py @@ -208,6 +208,22 @@ def process_image(im): scale = SMALL_SCALE, maxPixels = 1e10 ).get('water_map_NDWI')) + # Calculate red band/green band mean for water area in water_map_NDWI. + water_red_green_mean = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(RED_BAND_NAME)).divide(im.select( + GREEN_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.mean(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate nir band/red band mean for water area in water_map_NDWI. + water_nir_red_mean = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(NIR_BAND_NAME)).divide(im.select( + RED_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.mean(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) # Set attributes to retrive later im = im.set('cloud_area', cloud_area.multiply(1e-6)) @@ -219,6 +235,8 @@ def process_image(im): im = im.set('water_red_sum', water_red_sum) im = im.set('water_green_sum', water_green_sum) im = im.set('water_nir_sum', water_nir_sum) + im = im.set('water_red_green_mean', water_red_green_mean) + im = im.set('water_nir_red_mean', water_nir_red_mean) return im @@ -382,7 +400,7 @@ def run_process_long(res_name, res_polygon, start, end, datadir): res = generate_timeseries(dates).filterMetadata('l5_images', 'greater_than', 0) # Extracting uncorrected water area and other information from attributes uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l5_images', - 'water_red_sum', 'water_green_sum', 'water_nir_sum'] + 'water_red_sum', 'water_green_sum', 'water_nir_sum','water_red_green_mean','water_nir_red_mean'] uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') uncorrected_final_data = uncorrected_final_data_ee.getInfo() print("Uncorrected", uncorrected_final_data) diff --git a/src/rat/core/sarea/sarea_cli_l7.py b/src/rat/core/sarea/sarea_cli_l7.py index c85a1fdb..ce944261 100644 --- a/src/rat/core/sarea/sarea_cli_l7.py +++ b/src/rat/core/sarea/sarea_cli_l7.py @@ -208,6 +208,22 @@ def process_image(im): scale = SMALL_SCALE, maxPixels = 1e10 ).get('water_map_NDWI')) + # Calculate red band/green band mean for water area in water_map_NDWI. + water_red_green_mean = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(RED_BAND_NAME)).divide(im.select( + GREEN_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.mean(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate nir band/red band mean for water area in water_map_NDWI. + water_nir_red_mean = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(NIR_BAND_NAME)).divide(im.select( + RED_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.mean(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) # Set attributes to retrive later im = im.set('cloud_area', cloud_area.multiply(1e-6)) @@ -219,6 +235,8 @@ def process_image(im): im = im.set('water_red_sum', water_red_sum) im = im.set('water_green_sum', water_green_sum) im = im.set('water_nir_sum', water_nir_sum) + im = im.set('water_red_green_mean', water_red_green_mean) + im = im.set('water_nir_red_mean', water_nir_red_mean) return im @@ -416,7 +434,7 @@ def run_process_long(res_name, res_polygon, start, end, datadir): res = generate_timeseries(dates).filterMetadata('l7_images', 'greater_than', 0) # Extracting uncorrected water area and other information from attributes uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l7_images', - 'water_red_sum', 'water_green_sum', 'water_nir_sum'] + 'water_red_sum', 'water_green_sum', 'water_nir_sum','water_red_green_mean','water_nir_red_mean'] uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') uncorrected_final_data = uncorrected_final_data_ee.getInfo() print("Uncorrected", uncorrected_final_data) diff --git a/src/rat/core/sarea/sarea_cli_l8.py b/src/rat/core/sarea/sarea_cli_l8.py index d1c1164b..0125396f 100644 --- a/src/rat/core/sarea/sarea_cli_l8.py +++ b/src/rat/core/sarea/sarea_cli_l8.py @@ -240,6 +240,22 @@ def process_image(im): scale = SMALL_SCALE, maxPixels = 1e10 ).get('water_map_NDWI')) + # Calculate red band/green band mean for water area in water_map_NDWI. + water_red_green_mean = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(RED_BAND_NAME)).divide(im.select( + GREEN_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.mean(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate nir band/red band mean for water area in water_map_NDWI. + water_nir_red_mean = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(NIR_BAND_NAME)).divide(im.select( + RED_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.mean(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) im = im.set('cloud_area', cloud_area.multiply(1e-6)) im = im.set('cloud_percent', cloud_percent) @@ -250,6 +266,8 @@ def process_image(im): im = im.set('water_red_sum', water_red_sum) im = im.set('water_green_sum', water_green_sum) im = im.set('water_nir_sum', water_nir_sum) + im = im.set('water_red_green_mean', water_red_green_mean) + im = im.set('water_nir_red_mean', water_nir_red_mean) return im @@ -413,7 +431,7 @@ def run_process_long(res_name, res_polygon, start, end, datadir): # pprint.pprint(res.getInfo()) uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l8_images', - 'water_red_sum', 'water_green_sum', 'water_nir_sum'] + 'water_red_sum', 'water_green_sum', 'water_nir_sum','water_red_green_mean','water_nir_red_mean'] uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') uncorrected_final_data = uncorrected_final_data_ee.getInfo() print("Uncorrected", uncorrected_final_data) diff --git a/src/rat/core/sarea/sarea_cli_l9.py b/src/rat/core/sarea/sarea_cli_l9.py index dc6b57cc..1ac6a4b8 100644 --- a/src/rat/core/sarea/sarea_cli_l9.py +++ b/src/rat/core/sarea/sarea_cli_l9.py @@ -243,6 +243,22 @@ def process_image(im): scale = SMALL_SCALE, maxPixels = 1e10 ).get('water_map_NDWI')) + # Calculate red band/green band mean for water area in water_map_NDWI. + water_red_green_mean = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(RED_BAND_NAME)).divide(im.select( + GREEN_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.mean(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) + # Calculate nir band/red band mean for water area in water_map_NDWI. + water_nir_red_mean = ee.Number(im.select('water_map_NDWI').eq(1).multiply(im.select(NIR_BAND_NAME)).divide(im.select( + RED_BAND_NAME)).reduceRegion( + reducer = ee.Reducer.mean(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_NDWI')) im = im.set('cloud_area', cloud_area.multiply(1e-6)) im = im.set('cloud_percent', cloud_percent) @@ -253,6 +269,8 @@ def process_image(im): im = im.set('water_red_sum', water_red_sum) im = im.set('water_green_sum', water_green_sum) im = im.set('water_nir_sum', water_nir_sum) + im = im.set('water_red_green_mean', water_red_green_mean) + im = im.set('water_nir_red_mean', water_nir_red_mean) return im @@ -420,7 +438,7 @@ def run_process_long(res_name, res_polygon, start, end, datadir): # pprint.pprint(res.getInfo()) uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l9_images', - 'water_red_sum', 'water_green_sum', 'water_nir_sum'] + 'water_red_sum', 'water_green_sum', 'water_nir_sum','water_red_green_mean','water_nir_red_mean'] uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') uncorrected_final_data = uncorrected_final_data_ee.getInfo() print("Uncorrected", uncorrected_final_data) From 8069e143f32ed5630274c63d0175631e634fdd9b Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 11 Nov 2024 01:26:43 -0800 Subject: [PATCH 013/102] NSSC integration script from multiple sensors --- .../core/sarea/multisensor_ssc_integrator.py | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/rat/core/sarea/multisensor_ssc_integrator.py diff --git a/src/rat/core/sarea/multisensor_ssc_integrator.py b/src/rat/core/sarea/multisensor_ssc_integrator.py new file mode 100644 index 00000000..be058c24 --- /dev/null +++ b/src/rat/core/sarea/multisensor_ssc_integrator.py @@ -0,0 +1,141 @@ +import pandas as pd +import os +from sklearn.preprocessing import MinMaxScaler + +def multi_sensor_ssc_integration(l5_dfpath='', l7_dfpath='', l8_dfpath='', l9_dfpath='', s2_dfpath=''): + """ + Integrates Suspended Sediment Concentration (SSC) data from multiple satellite sources into a single DataFrame. + + This function reads SSC data from CSV files generated by different satellite sensors (Landsat 5, 7, 8, 9, and Sentinel-2), + merges the data into a unified DataFrame, and sorts it by date. Each row in the resulting DataFrame contains the SSC + values from one of the sensors, and an additional column indicates the satellite source. Only unique dates are retained, + with the first occurrence kept in cases of duplicates. + + Parameters: + ---------- + l5_dfpath : str, optional + Path to the CSV file for Landsat 5 data. Default is an empty string. + l7_dfpath : str, optional + Path to the CSV file for Landsat 7 data. Default is an empty string. + l8_dfpath : str, optional + Path to the CSV file for Landsat 8 data. Default is an empty string. + l9_dfpath : str, optional + Path to the CSV file for Landsat 9 data. Default is an empty string. + s2_dfpath : str, optional + Path to the CSV file for Sentinel-2 data. Default is an empty string. + + Returns: + ------- + pd.DataFrame + A DataFrame containing the combined SSC proxy ratios from the specified satellite sources. + Columns: + - date: The date of the SSC measurement. + - water_red_sum: Sum of red band reflectance values over water pixels. + - water_green_sum: Sum of green band reflectance values over water pixels. + - water_nir_sum: Sum of NIR band reflectance values over water pixels. + - water_red_green_mean: Mean of red/green ratio over water pixels. + - water_nir_red_mean: Mean of NIR/red ratio over water pixels. + - sat: The satellite source for each row (l5, l7, l8, l9, or s2). + + Raises: + ------ + ValueError + If none of the specified file paths exist or are provided. + + Notes: + ------ + - At least one valid file path must be provided for the function to execute successfully. + + Example Usage: + -------------- + >>> l5_path = 'path/to/l5_data.csv' + >>> l7_path = 'path/to/l7_data.csv' + >>> s2_path = 'path/to/s2_data.csv' + >>> merged_ssc_df = multi_sensor_ssc_integration(l5_dfpath=l5_path, l7_dfpath=l7_path, s2_dfpath=s2_path) + >>> print(merged_ssc_df.head()) + """ + + # Check that at least one valid file path is provided + if not any([os.path.isfile(p) for p in [l5_dfpath, l7_dfpath, l8_dfpath, l9_dfpath, s2_dfpath]]): + raise ValueError("At least one valid file path must be provided.") + + # Define a list to store DataFrames + dfs = [] + + # Load and process each file if it exists + for path, satellite in zip( + [l5_dfpath, l7_dfpath, l8_dfpath, l9_dfpath, s2_dfpath], + ['l5', 'l7', 'l8', 'l9', 's2'] + ): + if os.path.isfile(path): # Check if file exists + df = pd.read_csv(path) + # Rename date column for consistency + if satellite == 's2': + df = df.rename(columns={'date': 'mosaic_enddate'}) + df['date'] = pd.to_datetime(df['mosaic_enddate']) + df['sat'] = satellite # Add satellite column + # Select only relevant columns + df = df[['date', 'water_red_sum', 'water_green_sum', 'water_nir_sum', 'water_red_green_mean', 'water_nir_red_mean', 'sat']] + dfs.append(df) + + # Concatenate all DataFrames + merged_df = pd.concat(dfs, ignore_index=True) + + # Sort by date and drop duplicates keeping the first occurrence + merged_df = merged_df.sort_values(by='date').drop_duplicates(subset='date', keep='first').reset_index(drop=True) + + return merged_df + +def normalize_ssc(df): + """ + Creates Normalized Suspended Sediment Concentration (NSSC) values in a DataFrame using ratio of reflectance values. + + This function adds four new columns to the DataFrame by normalizing pixel-level and reservoir-level + SSC estimates. Pixel-level SSC ratios are normalized directly from `water_red_green_mean` and + `water_nir_red_mean`. Reservoir-level SSC ratios are calculated by dividing sum columns + (`water_red_sum`, `water_green_sum`, `water_nir_sum`) and then normalized. + + Parameters: + ---------- + df : pd.DataFrame + The DataFrame containing SSC data, with columns: + - water_red_sum + - water_green_sum + - water_nir_sum + - water_red_green_mean + - water_nir_red_mean + + Returns: + ------- + pd.DataFrame + The original DataFrame with the following new columns added: + - nssc_rd_gn_px: Normalized pixel-level SSC ratio (red/green). + - nssc_nr_rd_px: Normalized pixel-level SSC ratio (nir/red). + - nssc_rd_gn_res: Normalized reservoir-level SSC ratio (red/green). + - nssc_nr_rd_res: Normalized reservoir-level SSC ratio (nir/red). + """ + + # Initialize scaler + scaler = MinMaxScaler() + + ## Normalize pixel-level SSC ratios, ignoring NaNs + if df['water_red_green_mean'].notna().any(): + non_na_data = df['water_red_green_mean'].dropna().values.reshape(-1, 1) + df.loc[df['water_red_green_mean'].notna(), 'nssc_rd_gn_px'] = scaler.fit_transform(non_na_data).flatten() + + if df['water_nir_red_mean'].notna().any(): + non_na_data = df['water_nir_red_mean'].dropna().values.reshape(-1, 1) + df.loc[df['water_nir_red_mean'].notna(), 'nssc_nr_rd_px'] = scaler.fit_transform(non_na_data).flatten() + + # Calculate reservoir-level SSC ratios and normalize, ignoring NaNs + valid_ratio_rd_gn = (df['water_red_sum'] / df['water_green_sum']).dropna() + if valid_ratio_rd_gn.any(): + scaled_data = scaler.fit_transform(valid_ratio_rd_gn.values.reshape(-1, 1)).flatten() + df.loc[valid_ratio_rd_gn.index, 'nssc_rd_gn_res'] = scaled_data + + valid_ratio_nr_rd = (df['water_nir_sum'] / df['water_red_sum']).dropna() + if valid_ratio_nr_rd.any(): + scaled_data = scaler.fit_transform(valid_ratio_nr_rd.values.reshape(-1, 1)).flatten() + df.loc[valid_ratio_nr_rd.index, 'nssc_nr_rd_res'] = scaled_data + + return df \ No newline at end of file From 797bf6b4127eba726b8b39eae7cc176386a45d43 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 11 Nov 2024 01:27:14 -0800 Subject: [PATCH 014/102] Creating nssc alongside tmsos --- src/rat/core/run_sarea.py | 48 ++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/rat/core/run_sarea.py b/src/rat/core/run_sarea.py index 19ee230b..0f4cfccc 100644 --- a/src/rat/core/run_sarea.py +++ b/src/rat/core/run_sarea.py @@ -12,12 +12,14 @@ from rat.core.sarea.sarea_cli_sar import sarea_s1 from rat.core.sarea.bot_filter import bot_filter from rat.core.sarea.TMS import TMS +from rat.core.sarea.multisensor_ssc_integrator import multi_sensor_ssc_integration, normalize_ssc + log = getLogger(f"{LOG_NAME}.{__name__}") log_level1 = getLogger(f"{LOG_LEVEL1_NAME}.{__name__}") -def run_sarea(start_date, end_date, datadir, reservoirs_shpfile, shpfile_column_dict, filt_options = None): +def run_sarea(start_date, end_date, sarea_save_dir, reservoirs_shpfile, shpfile_column_dict, filt_options = None, nssc_save_dir = None): if isinstance(reservoirs_shpfile, gpd.GeoDataFrame): reservoirs_polygon = reservoirs_shpfile else: @@ -36,7 +38,7 @@ def run_sarea(start_date, end_date, datadir, reservoirs_shpfile, shpfile_column_ reservoir_area = float(reservoir[shpfile_column_dict['area_column']]) reservoir_polygon = reservoir.geometry log.info(f"Calculating surface area for {reservoir_name}.") - method = run_sarea_for_res(reservoir_name, reservoir_area, reservoir_polygon, start_date, end_date, datadir) + method = run_sarea_for_res(reservoir_name, reservoir_area, reservoir_polygon, start_date, end_date, sarea_save_dir, nssc_save_dir) log.info(f"Calculated surface area for {reservoir_name} successfully using {method} method.") if method == 'Optical': Optical_files += 1 @@ -62,46 +64,56 @@ def run_sarea(start_date, end_date, datadir, reservoirs_shpfile, shpfile_column_ log_level1.error(f"BOT Filter run failed for all reservoirs.") log_level1.error("Filter values out of bounds. Please ensure that a value in between 0 and 9 is selected") else: - bot_filter(datadir,shpfile_column_dict,reservoirs_shpfile,**filt_options) + bot_filter(sarea_save_dir,shpfile_column_dict,reservoirs_shpfile,**filt_options) -def run_sarea_for_res(reservoir_name, reservoir_area, reservoir_polygon, start_date, end_date, datadir): +def run_sarea_for_res(reservoir_name, reservoir_area, reservoir_polygon, start_date, end_date, sarea_save_dir, nssc_save_dir): # Obtain surface areas # Sentinel-2 log.debug(f"Reservoir: {reservoir_name}; Downloading Sentinel-2 data from {start_date} to {end_date}") - sarea_s2(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(datadir, 's2')) - s2_dfpath = os.path.join(datadir, 's2', reservoir_name+'.csv') + sarea_s2(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(sarea_save_dir, 's2')) + s2_dfpath = os.path.join(sarea_save_dir, 's2', reservoir_name+'.csv') # Landsat-5 log.debug(f"Reservoir: {reservoir_name}; Downloading Landsat-5 data from {start_date} to {end_date}") - sarea_l5(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(datadir, 'l5')) - l5_dfpath = os.path.join(datadir, 'l5', reservoir_name+'.csv') + sarea_l5(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(sarea_save_dir, 'l5')) + l5_dfpath = os.path.join(sarea_save_dir, 'l5', reservoir_name+'.csv') # Landsat-7 log.debug(f"Reservoir: {reservoir_name}; Downloading Landsat-7 data from {start_date} to {end_date}") - sarea_l7(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(datadir, 'l7')) - l7_dfpath = os.path.join(datadir, 'l7', reservoir_name+'.csv') + sarea_l7(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(sarea_save_dir, 'l7')) + l7_dfpath = os.path.join(sarea_save_dir, 'l7', reservoir_name+'.csv') # Landsat-8 log.debug(f"Reservoir: {reservoir_name}; Downloading Landsat-8 data from {start_date} to {end_date}") - sarea_l8(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(datadir, 'l8')) - l8_dfpath = os.path.join(datadir, 'l8', reservoir_name+'.csv') + sarea_l8(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(sarea_save_dir, 'l8')) + l8_dfpath = os.path.join(sarea_save_dir, 'l8', reservoir_name+'.csv') # Landsat-9 log.debug(f"Reservoir: {reservoir_name}; Downloading Landsat-9 data from {start_date} to {end_date}") - sarea_l9(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(datadir, 'l9')) - l9_dfpath = os.path.join(datadir, 'l9', reservoir_name+'.csv') + sarea_l9(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(sarea_save_dir, 'l9')) + l9_dfpath = os.path.join(sarea_save_dir, 'l9', reservoir_name+'.csv') # Sentinel-1 log.debug(f"Reservoir: {reservoir_name}; Downloading Sentinel-1 data from {start_date} to {end_date}") - s1_dfpath = sarea_s1(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(datadir, 'sar')) - s1_dfpath = os.path.join(datadir, 'sar', reservoir_name+'_12d_sar.csv') + s1_dfpath = sarea_s1(reservoir_name, reservoir_polygon, start_date, end_date, os.path.join(sarea_save_dir, 'sar')) + s1_dfpath = os.path.join(sarea_save_dir, 'sar', reservoir_name+'_12d_sar.csv') + # Using TMSOS to ensemble surface area data tmsos = TMS(reservoir_name, reservoir_area) result,method = tmsos.tms_os(l5_dfpath=l5_dfpath, l7_dfpath=l7_dfpath, l9_dfpath=l9_dfpath, l8_dfpath=l8_dfpath, s2_dfpath=s2_dfpath, s1_dfpath=s1_dfpath) - - tmsos_savepath = os.path.join(datadir, reservoir_name+'.csv') + tmsos_savepath = os.path.join(sarea_save_dir, reservoir_name+'.csv') log.debug(f"Saving surface area of {reservoir_name} at {tmsos_savepath}") result.reset_index().rename({'index': 'date', 'filled_area': 'area'}, axis=1).to_csv(tmsos_savepath, index=False) + + # NSSC calculations + if nssc_save_dir: + ssc_components_df = multi_sensor_ssc_integration(l5_dfpath=l5_dfpath, l7_dfpath=l7_dfpath, l9_dfpath=l9_dfpath, l8_dfpath=l8_dfpath, + s2_dfpath=s2_dfpath) + nssc_df = normalize_ssc(ssc_components_df) + nssc_savepath = os.path.join(nssc_save_dir, reservoir_name+'.csv') + log.debug(f"Saving NSSC data of {reservoir_name} at {nssc_savepath}") + nssc_df.to_csv(nssc_savepath, index=False) + return method From a2ccb8a0cf7ee8fedc5ea4bb9d22c32ce9c12377 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 11 Nov 2024 01:27:55 -0800 Subject: [PATCH 015/102] Step 13 creating nssc files in rat outputs --- src/rat/core/run_postprocessing.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/rat/core/run_postprocessing.py b/src/rat/core/run_postprocessing.py index 48b3883a..78ff0227 100644 --- a/src/rat/core/run_postprocessing.py +++ b/src/rat/core/run_postprocessing.py @@ -180,7 +180,7 @@ def calc_outflow(inflowpath, dspath, epath, area, savepath): def run_postprocessing(basin_name, basin_data_dir, reservoir_shpfile, reservoir_shpfile_column_dict, aec_dir_path, start_date, end_date, rout_init_state_save_file, use_rout_state, - evap_datadir, dels_savedir, outflow_savedir, vic_status, routing_status, gee_status, forecast_mode=False): + evap_datadir, dels_savedir, nssc_savedir, outflow_savedir, vic_status, routing_status, gee_status, forecast_mode=False): # read file defining mapped resrvoirs # reservoirs_fn = os.path.join(project_dir, 'backend/data/ancillary/RAT-Reservoirs.geojson') reservoirs = gpd.read_file(reservoir_shpfile) @@ -197,9 +197,10 @@ def run_postprocessing(basin_name, basin_data_dir, reservoir_shpfile, reservoir_ # SArea sarea_raw_dir = os.path.join(basin_data_dir,'gee', "gee_sarea_tmsos") + nssc_raw_dir = os.path.join(basin_data_dir,'gee', "gee_nssc") ## No of failed files (no_failed_files) is tracked and used to print a warning message in log level 1 file. - # DelS + # DelS calculation & copying of NSSC files if(gee_status): log.debug("Calculating ∆S") no_failed_files = 0 @@ -210,14 +211,22 @@ def run_postprocessing(basin_name, basin_data_dir, reservoir_shpfile, reservoir_ # Reading reservoir information reservoir_name = str(reservoir[reservoir_shpfile_column_dict['unique_identifier']]) sarea_path = os.path.join(sarea_raw_dir, reservoir_name + ".csv") - savepath = os.path.join(dels_savedir, reservoir_name + ".csv") + nssc_path = os.path.join(nssc_raw_dir, reservoir_name + ".csv") + dels_savepath = os.path.join(dels_savedir, reservoir_name + ".csv") + nssc_savepath = os.path.join(nssc_savedir, reservoir_name + ".csv") aecpath = os.path.join(aec_dir, reservoir_name + ".csv") if os.path.isfile(sarea_path): - log.debug(f"Calculating ∆S for {reservoir_name}, saving at: {savepath}") - calc_dels(aecpath, sarea_path, savepath) + log.debug(f"Calculating ∆S for {reservoir_name}, saving at: {dels_savepath}") + calc_dels(aecpath, sarea_path, dels_savepath) else: raise Exception("Surface area file not found; skipping ∆S calculation") + + if os.path.isfile(nssc_path): + nssc_df = pd.read_csv(nssc_path) + nssc_df.to_csv(nssc_savepath, index=False) + else: + raise Exception("NSSC file not found for {reservoir_name}; skipping copy to RAT Outputs") except: log.exception(f"∆S for {reservoir_name} could not be calculated.") no_failed_files += 1 From 16a0c10261011948cd9659a070de6d60bf2c2469 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 11 Nov 2024 01:28:19 -0800 Subject: [PATCH 016/102] Step 14 convert nssc to final outputs --- src/rat/rat_basin.py | 14 +++++++++----- src/rat/utils/convert_to_final_outputs.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/rat/rat_basin.py b/src/rat/rat_basin.py index b99223e6..8fc7dbf7 100644 --- a/src/rat/rat_basin.py +++ b/src/rat/rat_basin.py @@ -32,7 +32,7 @@ from rat.core.run_postprocessing import run_postprocessing -from rat.utils.convert_to_final_outputs import convert_sarea, convert_inflow, convert_dels, convert_evaporation, convert_outflow, convert_altimeter, copy_aec_files +from rat.utils.convert_to_final_outputs import convert_sarea, convert_inflow, convert_dels, convert_evaporation, convert_outflow, convert_altimeter, copy_aec_files, convert_nssc # Step-(-1): Reading Configuration settings to run RAT # Step-0: Creating required directory structure for RAT @@ -335,6 +335,7 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base reservoirs_gdf_column_dict['unique_identifier'] = reservoirs_gdf_column_dict['dam_name_column'] # Defining paths to save surface area from gee and heights from altimetry sarea_savepath = create_directory(os.path.join(basin_data_dir,'gee','gee_sarea_tmsos',''), True) + nssc_savepath = create_directory(os.path.join(basin_data_dir,'gee','gee_nssc',''), True) altimetry_savepath = os.path.join(basin_data_dir,'altimetry','altimetry_timeseries') #----------- Paths Necessary for running of Surface Area Calculation and Altimetry-----------# @@ -350,6 +351,7 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base else: evap_savedir = create_directory(os.path.join(basin_data_dir,'rat_outputs', 'Evaporation'), True) dels_savedir = create_directory(os.path.join(basin_data_dir,'rat_outputs', "dels"), True) + nssc_savedir = create_directory(os.path.join(basin_data_dir,'rat_outputs', "nssc"), True) outflow_savedir = create_directory(os.path.join(basin_data_dir,'rat_outputs', "rat_outflow"),True) aec_savedir = Path(create_directory(os.path.join(basin_data_dir,'rat_outputs', "aec"),True)) final_output_path = create_directory(os.path.join(basin_data_dir,'final_outputs',''),True) @@ -699,7 +701,7 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base # Get Sarea filt_options = config['GEE'].get('bot_filter') run_sarea(gee_start_date.strftime("%Y-%m-%d"), config['BASIN']['end'].strftime("%Y-%m-%d"), sarea_savepath, - basin_reservoir_shpfile_path, reservoirs_gdf_column_dict,filt_options) + basin_reservoir_shpfile_path, reservoirs_gdf_column_dict,filt_options,nssc_savepath) GEE_STATUS = 1 except: no_errors = no_errors+1 @@ -761,7 +763,8 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base rat_logger.warning("AEC files could not be copied to rat_outputs directory.", exc_info=True) #Generating evaporation, storage change and outflow. DELS_STATUS, EVAP_STATUS, OUTFLOW_STATUS = run_postprocessing(basin_name, basin_data_dir, basin_reservoir_shpfile_path, reservoirs_gdf_column_dict, - aec_dir_path, config['BASIN']['start'], config['BASIN']['end'], rout_init_state_save_file, use_state, evap_savedir, dels_savedir, outflow_savedir, VIC_STATUS, ROUTING_STATUS, GEE_STATUS, forecast_mode=forecast_mode) + aec_dir_path, config['BASIN']['start'], config['BASIN']['end'], rout_init_state_save_file, use_state, evap_savedir, dels_savedir, + nssc_savedir, outflow_savedir, VIC_STATUS, ROUTING_STATUS, GEE_STATUS, forecast_mode=forecast_mode) except: no_errors = no_errors+1 rat_logger.exception("Error Executing Step-13: Calculation of Outflow, Evaporation, Storage change and Inflow") @@ -778,9 +781,10 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base ## Surface Area if(GEE_STATUS): convert_sarea(sarea_savepath,final_output_path) - rat_logger.info("Converted Surface Area to the Output Format.") + convert_nssc(nssc_savepath,final_output_path) + rat_logger.info("Converted Surface Area and NSSC to the Output Format.") else: - rat_logger.info("Could not convert Surface Area to the Output Format as GEE run failed.") + rat_logger.info("Could not convert Surface Area and NSSC to the Output Format as GEE run failed.") ## Inflow if(ROUTING_STATUS): diff --git a/src/rat/utils/convert_to_final_outputs.py b/src/rat/utils/convert_to_final_outputs.py index 327364ba..1ec5869d 100644 --- a/src/rat/utils/convert_to_final_outputs.py +++ b/src/rat/utils/convert_to_final_outputs.py @@ -135,6 +135,21 @@ def copy_aec_files(src_dir, dst_dir): 'Elevation_Observed': 'elevation_srtm' }, axis=1, inplace=True) aec.to_csv(dst_dir / src_path.name, index=False) + +def convert_nssc(nssc_dir, final_out_dir): + src_dir = Path(nssc_dir) + dst_dir = Path(create_directory(os.path.join(final_out_dir,'nssc'),True)) + + for src_path in src_dir.glob('*.csv'): + nssc_df = pd.read_csv(src_path) + df_to_save = nssc_df[['date','nssc_rd_gn_px','nssc_nr_rd_px', 'nssc_rd_gn_res', 'nssc_nr_rd_res']] + df_to_save.rename({ + 'nssc_rd_gn_px': 'NSSC (red/green per pixel)', + 'nssc_nr_rd_px': 'NSSC (nir/red per pixel)', + 'nssc_rd_gn_res': 'NSSC (total_red/total_green)', + 'nssc_nr_rd_res': 'NSSC (total_nir/totaL_red)', + }, axis=1, inplace=True) + df_to_save.to_csv(dst_dir / src_path.name, index=False) def convert_v2_frontend(basin_data_dir, res_name, inflow_src, sarea_src, dels_src, outflow_src): From 91f55fc22dbec9df0c2b99bc55aa8b461c63d51f Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 29 Nov 2024 16:45:51 -0800 Subject: [PATCH 017/102] Added code to create climate TS for catchment --- src/rat/toolbox/data_transform.py | 125 ++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/rat/toolbox/data_transform.py diff --git a/src/rat/toolbox/data_transform.py b/src/rat/toolbox/data_transform.py new file mode 100644 index 00000000..3c28f53d --- /dev/null +++ b/src/rat/toolbox/data_transform.py @@ -0,0 +1,125 @@ +import geopandas as gpd +import os +import xarray as xr +import rioxarray as rxr +import pandas as pd +from shapely.geometry import mapping + +def create_meterological_ts(roi, nc_file_path, output_csv_path): + """ + Create a meteorological timeseries for a given region of interest (ROI) using combined meteorological + data stored in a NetCDF file produced by RAT, and save the output as a CSV file. + + This function extracts time-series data for the variables 'precip', 'tmin', 'tmax', and 'wind' for the specified + geometry (ROI), calculates the spatial average for each time step, and stores the results in a CSV file. + + Parameters: + ---------- + roi : shapely.geometry.Polygon + A shapely geometry representing the region of interest (ROI) for which the meteorological + timeseries will be extracted. The geometry should be in the WGS84 coordinate reference system (CRS), same as the + NetCDF dataset. + + nc_file_path : str + Path to the NetCDF file containing combined meteorological data produced by RAT. The NetCDF file should include + the variables 'precip', 'tmin', 'tmax', and 'wind', and have appropriate spatial dimensions such as 'lat' and 'lon'. + or 'longitude' and 'latitude'. If crs is not specified, it is assumed to be a WGS84. + + output_csv_path : str + Path where the output CSV file will be saved (or appended in case, file exists). The file will contain the time series data for the spatially averaged + values of the meteorological variables ('precip', 'tmin', 'tmax', 'wind'). + + Returns: + ------- + None + The function does not return any value but saves the meteorological time series to the specified CSV file. + + Raises: + ------ + ValueError : + If the spatial dimensions ('lon', 'lat', 'longitude', or 'latitude') are not found in the NetCDF dataset. + + Warning : + If the CRS of the GeoDataFrame is not set, appropriate warnings will be raised and file will not be created. + + Notes: + ------ + - The function clips the dataset based on the ROI geometry and calculates the spatial average for each time step. + - The output CSV will contain columns for 'time', 'precip', 'tmin', 'tmax', and 'wind' with their spatially averaged values. + - If the output CSV exists, data will be appended and in case of duplicate dates, latest data will be kept for each date. + + Example: + -------- + # Create a GeoDataFrame representing the ROI (Polygon) + roi = geopandas.GeoDataFrame(...) + + # Specify the path to the NetCDF file + nc_file_path = 'path_to_netcdf_file.nc' + + # Specify the path to save the output CSV + output_csv_path = 'output_timeseries.csv' + + # Create the meteorological time series and save it to CSV + create_meterological_ts(roi, nc_file_path, output_csv_path) + """ + + print("Creating meterological timeseries for a given geometry using comibined meteorlogical NetCDF produced by RAT.") + # Load the NetCDF file as an xarray Dataset + ds = xr.open_dataset(nc_file_path) + + # Ensure spatial dimensions are set correctly as x and y for rioxarray use + if 'lon' in ds.dims and 'lat' in ds.dims: + ds = ds.rename({'lon': 'x', 'lat': 'y'}) + elif 'longitude' in ds.dims and 'latitude' in ds.dims: + ds = ds.rename({'longitude': 'x', 'latitude': 'y'}) + else: + raise ValueError("Spatial dimensions not found. Expected 'lon', 'lat', 'longitude', or 'latitude'.") + + # Set spatial dimensions + ds = ds.rio.set_spatial_dims(x_dim='x', y_dim='y') + + # Sets default CRS for the dataset if missing + if ds.rio.crs is None: + print("CRS not found for dataset. Setting CRS to EPSG:4326.") + ds.rio.write_crs("EPSG:4326", inplace=True) + + # Set CRS for GeoDataFrame + if gdf.crs is None: + print("CRS not found for GeoDataFrame. Please set it manually.") + return None + + # Convert the combined geometry to a format that rioxarray can work with + geometries = [mapping(roi)] + + # Clip the xarray Dataset using the ROI geometry + try: + ds_clipped = ds.rio.clip(geometries, from_disk=True) + except Exception as e: + print(f"Error during clipping: {e}") + return + + # Calculate the spatial average for each time step + spatial_mean = ds_clipped.mean(dim=['x', 'y']) + + # Convert the spatial mean to a pandas DataFrame + df = pd.DataFrame({ + 'time': spatial_mean.time.values, + 'precip': spatial_mean.precip.values, + 'tmin': spatial_mean.tmin.values, + 'tmax': spatial_mean.tmax.values, + 'wind': spatial_mean.wind.values + }) + + # Append the data if file exists + if os.path.exists(output_csv_path): + print(f"File {output_csv_path} exists. Appending new data and removing duplicates.") + existing_df = pd.read_csv(output_csv_path, parse_dates=['time']) + combined_df = pd.concat([existing_df, df], ignore_index=True) + # Remove duplicates, keeping the latest entry for each date + combined_df = combined_df.sort_values(by='time').drop_duplicates(subset='time', keep='last') + combined_df.to_csv(output_csv_path, index=False) + else: + # Save the new data + df.to_csv(output_csv_path, index=False) + + print(f"CSV file has been updated and saved to {output_csv_path}.") \ No newline at end of file From c4ced3e44eff65eacd44c9b0d41239a29aaf7330 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 29 Nov 2024 16:46:20 -0800 Subject: [PATCH 018/102] Addition of final output of climate TS for catchment --- src/rat/utils/convert_to_final_outputs.py | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/rat/utils/convert_to_final_outputs.py b/src/rat/utils/convert_to_final_outputs.py index 1ec5869d..84fca69e 100644 --- a/src/rat/utils/convert_to_final_outputs.py +++ b/src/rat/utils/convert_to_final_outputs.py @@ -4,6 +4,7 @@ import numpy as np from pathlib import Path from rat.utils.utils import create_directory +from rat.toolbox.data_transform import create_meterological_ts def convert_sarea(sarea_dir, website_v_dir): # Surface Area @@ -151,6 +152,32 @@ def convert_nssc(nssc_dir, final_out_dir): }, axis=1, inplace=True) df_to_save.to_csv(dst_dir / src_path.name, index=False) +def convert_meteorological_ts(catchment_shpfile, catchments_gdf_column_dict, basin_gpd_df, meteorological_nc_file_path, final_out_dir): + if catchment_shpfile: + print('Reading Catchment Shapefile') + catchments = gpd.read_file(catchment_shpfile) + dst_dir = Path(create_directory(os.path.join(final_out_dir,'catchment_climate'),True)) + + if catchments_gdf_column_dict['unique_identifier'] == 'uniq_id': + print('Finding a spatial join of catchments because of Global Station Vector File') + basin_data_crs_changed = basin_gpd_df.to_crs(catchments.crs) + catchments_spatial_filtered = gpd.sjoin(catchments, basin_data_crs_changed, "inner")[catchments.columns] + catchments_spatial_filtered['uniq_id'] = catchments_spatial_filtered[catchments_gdf_column_dict['id_column']].astype(str)+'_'+ \ + catchments_spatial_filtered[catchments_gdf_column_dict['dam_name_column']].astype(str).str.replace(' ','_') + else: + catchments_spatial_filtered = catchments.copy() + + for index, row in catchments_spatial_filtered.iterrows(): + res_name = row[catchments_gdf_column_dict['unique_identifier']] + save_file_path = dst_dir / res_name / '.csv' + catchment_roi = row['geometry'] + print(f"Creating Catchment's Climatolgical time series for reservoir : {res_name}") + create_meterological_ts(catchment_roi, meteorological_nc_file_path, save_file_path) + + return + else: + return + def convert_v2_frontend(basin_data_dir, res_name, inflow_src, sarea_src, dels_src, outflow_src): """Converts the files according to the newer version of the frontend (v2). From 69a1855993dc5215732e9a3968e15a3c72f106fd Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 29 Nov 2024 16:46:42 -0800 Subject: [PATCH 019/102] Added necessary paths and sclimate TS in step14 --- src/rat/rat_basin.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/rat/rat_basin.py b/src/rat/rat_basin.py index 8fc7dbf7..eb51afce 100644 --- a/src/rat/rat_basin.py +++ b/src/rat/rat_basin.py @@ -32,7 +32,7 @@ from rat.core.run_postprocessing import run_postprocessing -from rat.utils.convert_to_final_outputs import convert_sarea, convert_inflow, convert_dels, convert_evaporation, convert_outflow, convert_altimeter, copy_aec_files, convert_nssc +from rat.utils.convert_to_final_outputs import convert_sarea, convert_inflow, convert_dels, convert_evaporation, convert_outflow, convert_altimeter, copy_aec_files, convert_nssc, convert_meteorological_ts # Step-(-1): Reading Configuration settings to run RAT # Step-0: Creating required directory structure for RAT @@ -345,6 +345,20 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base aec_dir_path = config['POST_PROCESSING'].get('aec_dir') else: aec_dir_path = create_directory(os.path.join(basin_data_dir,'post_processing','post_processing_gee_aec',''), True) + # Defining paths for creating meteorlogical timeseries for reservoir catchment + if (config['POST_PROCESSING'].get('catchment_vector_file')): + catchment_vector_file_path = config['POST_PROCESSING'].get('catchment_vector_file') + catchments_gdf_column_dict = config['POST_PROCESSING'].get('catchment_vector_file_columns_dict') + # Adding key-value pair to Basin Reservoir Shapefile's column dictionary ### + if (config['ROUTING']['station_global_data']): + catchments_gdf_column_dict['unique_identifier'] = 'uniq_id' + else: + catchments_gdf_column_dict['unique_identifier'] = catchments_gdf_column_dict['dam_name_column'] + else: + catchment_vector_file_path = None + catchments_gdf_column_dict = None + # Reading catchment vector file's column dictionary + ## Paths for storing post-processed data and in webformat data if forecast_mode: evap_savedir = create_directory(os.path.join(basin_data_dir,'rat_outputs', 'forecast_evaporation', f"{forecast_basedate:%Y%m%d}"), True) @@ -793,6 +807,13 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base else: rat_logger.info("Could not convert Inflow to the Output Format as Routing run failed.") + ## Climatological TS + if (ROUTING_STATUS): + convert_meteorological_ts(catchment_vector_file_path, catchments_gdf_column_dict, basin_data, combined_datapath, final_output_path) + rat_logger.info("Converted Catchment's Climatological TS to the Output Format (from NetCDF).") + else: + rat_logger.info("Could not convert Catchment's Climatological TS to the Output Format (from NetCDF) as Routing run failed.") + ## Dels if(DELS_STATUS): convert_dels(dels_savedir, final_output_path) From 0d73fc63b308f1f5c5bea7cff7d475bcec4295fe Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sat, 21 Dec 2024 18:04:16 -0800 Subject: [PATCH 020/102] Error handling for cases where reservoir is small and clustering or zhao-gao correction not possible --- src/rat/core/sarea/sarea_cli_l5.py | 145 +++++++++++++++++--------- src/rat/core/sarea/sarea_cli_l7.py | 160 ++++++++++++++++++++--------- src/rat/core/sarea/sarea_cli_l8.py | 146 +++++++++++++++++--------- src/rat/core/sarea/sarea_cli_l9.py | 143 ++++++++++++++++++-------- 4 files changed, 411 insertions(+), 183 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_l5.py b/src/rat/core/sarea/sarea_cli_l5.py index aaad3275..84c7204f 100644 --- a/src/rat/core/sarea/sarea_cli_l5.py +++ b/src/rat/core/sarea/sarea_cli_l5.py @@ -12,6 +12,8 @@ from logging import getLogger # Defining global constants +log = getLogger(f"{LOG_NAME}.{__name__}") + l5 = ee.ImageCollection("LANDSAT/LT05/C02/T1_L2") gswd = ee.Image("JRC/GSW1_4/GlobalSurfaceWater") @@ -61,17 +63,10 @@ def preprocess(im): ## Processing individual images - water classification ## ##########################################################/ -def identify_water_cluster(im): +def identify_water_cluster(im, max_cluster_value): im = ee.Image(im) mbwi = im.select('MBWI') - - max_cluster_value = ee.Number(im.select('cluster').reduceRegion( - reducer = ee.Reducer.max(), - geometry = aoi, - scale = LARGE_SCALE, - maxPixels = 1e10 - ).get('cluster')) - + clusters = ee.List.sequence(0, max_cluster_value) def calc_avg_mbwi(cluster_val): @@ -82,8 +77,11 @@ def calc_avg_mbwi(cluster_val): geometry = aoi, maxPixels = 1e10 ).get('MBWI') - return avg_mbwi - + avg_mbwi_not_null = ee.Number(ee.Algorithms.If(ee.Algorithms.IsEqual(ee.Number(avg_mbwi)), + ee.Number(-99), + ee.Number(avg_mbwi))) + return avg_mbwi_not_null + # print(calc_avg_mbwi(clusters.get(0)).getInfo()) avg_mbwis = ee.Array(clusters.map(calc_avg_mbwi)) max_mbwi_index = avg_mbwis.argmax().get(0) @@ -94,6 +92,9 @@ def calc_avg_mbwi(cluster_val): def cordeiro(im): + ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on + ## calinski harabasz's work + ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans band_subset = ee.List(['NDWI', 'MNDWI', SWIR2_BAND_NAME]) # using NDWI, MNDWI and B7 (SWIR2) sampled_pts = im.select(band_subset).sample( region = aoi, @@ -101,22 +102,58 @@ def cordeiro(im): numPixels = 5e3-1 ## limit of 5k points ) - ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on - ## calinski harabasz's work - ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans - clusterer = ee.Clusterer.wekaCascadeKMeans( - minClusters = 2, - maxClusters = 7, - init = True - ).train(sampled_pts) + no_sampled_pts = sampled_pts.size() + + def if_enough_sample_pts(im): + clusterer = ee.Clusterer.wekaCascadeKMeans( + minClusters = 2, + maxClusters = 7, + init = True + ).train(sampled_pts) + + classified = im.select(band_subset).cluster(clusterer) + im = im.addBands(classified) + max_cluster_value = ee.Number(im.select('cluster').reduceRegion( + reducer = ee.Reducer.max(), + geometry = aoi, + scale = LARGE_SCALE, + maxPixels = 1e10 + ).get('cluster')) + return ee.Dictionary({'max_cluster_value': max_cluster_value, 'classified': classified}) + + def if_not_enough_sample_pts(): + return ee.Dictionary({'max_cluster_value': ee.Number(0), 'classified': ee.Image(0).rename('cluster')}) + + # If clustering is possible do clustering + def if_clustering_possible(max_cluster_value,classified,im): + im = im.addBands(classified) + + water_cluster = identify_water_cluster(im, max_cluster_value) + water_map = classified.select('cluster').eq(ee.Image.constant(water_cluster)).select(['cluster'], ['water_map_cordeiro']) + return water_map + + # If no clustering is possible, use NDWI water map + def if_clustering_not_possible(im): + water_map = im.select(['water_map_NDWI'],['water_map_cordeiro']) + return water_map + - # Classify clusters - classified = im.select(band_subset).cluster(clusterer) - im = im.addBands(classified) + after_training_dict = ee.Dictionary(ee.Algorithms.If(ee.Number(no_sampled_pts), + if_enough_sample_pts(im), + if_not_enough_sample_pts())) - # Select cluster with highest average MBWI and say it as water map cordeiro - water_cluster = identify_water_cluster(im) - water_map = classified.select('cluster').eq(ee.Image.constant(water_cluster)).select(['cluster'], ['water_map_cordeiro']) + max_cluster_value = ee.Number(ee.Algorithms.If(ee.Algorithms.IsEqual( + ee.Number(after_training_dict.get('max_cluster_value'))), + ee.Number(0), + ee.Number(after_training_dict.get('max_cluster_value')))) + + classified = ee.Image(after_training_dict.get('classified')) + water_map = ee.Image(ee.Algorithms.If(ee.Algorithms.IsEqual(max_cluster_value,ee.Number(0)), + if_clustering_not_possible(im), + if_clustering_possible(max_cluster_value, + classified, + im) + )) im = im.addBands(water_map) return im @@ -146,7 +183,8 @@ def process_image(im): cloud_percent = cloud_area.multiply(100).divide(aoi.area()) cordeiro_will_run_when = cloud_percent.lt(CLOUD_COVER_LIMIT) - + # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. + im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) # Clustering based classification of water pixels for cloud pixels when cloud cover is less than CLOUD_COVER_LIMIT im = im.addBands(ee.Image(ee.Algorithms.If(cordeiro_will_run_when, cordeiro(im), ee.Image.constant(-1e6)))) # run cordeiro only if cloud percent is < 90% @@ -171,8 +209,6 @@ def process_image(im): ee.Number(-1e6) )) - # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. - im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) # Calculate water area in water_map_NDWI. water_area_NDWI = ee.Number(im.select('water_map_NDWI').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), @@ -256,27 +292,44 @@ def postprocess(): maxPixels = 1e10 ).get('occurrence')) - counts = ee.Array(hist).transpose().toList() - - omega = ee.Number(0.17) - count_thresh = ee.Number(counts.map(lambda lis: ee.List(lis).reduce(ee.Reducer.mean())).get(1)).multiply(omega) - - count_thresh_index = ee.Array(counts.get(1)).gt(count_thresh).toList().indexOf(1) - occurrence_thresh = ee.Number(ee.List(counts.get(0)).get(count_thresh_index)) - - water_map = im.select([bandName], ['water_map']) - gswd_improvement = gswd.clip(aoi).gte(occurrence_thresh).updateMask(water_map.mask().Not()).select(["occurrence"], ["water_map"]) + def if_hist_not_null(im,hist): + + counts = ee.Array(hist).transpose().toList() + + omega = ee.Number(0.17) + count_thresh = ee.Number(counts.map(lambda lis: ee.List(lis).reduce(ee.Reducer.mean())).get(1)).multiply(omega) + + count_thresh_index = ee.Array(counts.get(1)).gt(count_thresh).toList().indexOf(1) + occurrence_thresh = ee.Number(ee.List(counts.get(0)).get(count_thresh_index)) + + water_map = im.select([bandName], ['water_map']) + gswd_improvement = gswd.clip(aoi).gte(occurrence_thresh).updateMask(water_map.mask().Not()).select(["occurrence"], ["water_map"]) + + improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) + + corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_zhao_gao')) + + improved = improved.set("corrected_area", corrected_area.multiply(1e-6)); + improved = improved.set("POSTPROCESSING_SUCCESSFUL", 1); - improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) + return improved - corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( - reducer = ee.Reducer.sum(), - geometry = aoi, - scale = SMALL_SCALE, - maxPixels = 1e10 - ).get('water_map_zhao_gao')) + def if_hist_null(im): + # Preserve the uncorrected area in the corrected area column & POSTPROCESSING=1 + # because otherwise it will be substituted with nan. + uncorrected_area = ee.Number(im.get('water_area_clustering')) + improved = im.set('corrected_area', uncorrected_area) + improved = improved.set('POSTPROCESSING_SUCCESSFUL', 1) + return improved - improved = improved.set("corrected_area", corrected_area.multiply(1e-6)) + improved = ee.Image(ee.Algorithms.If( + hist, if_hist_not_null(im,hist), if_hist_null(im) + )) return improved def dont_post_process(): diff --git a/src/rat/core/sarea/sarea_cli_l7.py b/src/rat/core/sarea/sarea_cli_l7.py index ce944261..8c30ee70 100644 --- a/src/rat/core/sarea/sarea_cli_l7.py +++ b/src/rat/core/sarea/sarea_cli_l7.py @@ -12,6 +12,8 @@ from logging import getLogger # Defining global constants +log = getLogger(f"{LOG_NAME}.{__name__}") + l7= ee.ImageCollection("LANDSAT/LE07/C02/T1_L2") gswd = ee.Image("JRC/GSW1_4/GlobalSurfaceWater") @@ -59,17 +61,10 @@ def preprocess(im): ## Processing individual images - water classification ## ##########################################################/ -def identify_water_cluster(im): +def identify_water_cluster(im, max_cluster_value): im = ee.Image(im) mbwi = im.select('MBWI') - - max_cluster_value = ee.Number(im.select('cluster').reduceRegion( - reducer = ee.Reducer.max(), - geometry = aoi, - scale = LARGE_SCALE, - maxPixels = 1e10 - ).get('cluster')) - + clusters = ee.List.sequence(0, max_cluster_value) def calc_avg_mbwi(cluster_val): @@ -80,8 +75,11 @@ def calc_avg_mbwi(cluster_val): geometry = aoi, maxPixels = 1e10 ).get('MBWI') - return avg_mbwi - + avg_mbwi_not_null = ee.Number(ee.Algorithms.If(ee.Algorithms.IsEqual(ee.Number(avg_mbwi)), + ee.Number(-99), + ee.Number(avg_mbwi))) + return avg_mbwi_not_null + # print(calc_avg_mbwi(clusters.get(0)).getInfo()) avg_mbwis = ee.Array(clusters.map(calc_avg_mbwi)) max_mbwi_index = avg_mbwis.argmax().get(0) @@ -92,6 +90,9 @@ def calc_avg_mbwi(cluster_val): def cordeiro(im): + ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on + ## calinski harabasz's work + ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans band_subset = ee.List(['NDWI', 'MNDWI', SWIR2_BAND_NAME]) # using NDWI, MNDWI and B7 (SWIR2) sampled_pts = im.select(band_subset).sample( region = aoi, @@ -99,22 +100,58 @@ def cordeiro(im): numPixels = 5e3-1 ## limit of 5k points ) - ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on - ## calinski harabasz's work - ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans - clusterer = ee.Clusterer.wekaCascadeKMeans( - minClusters = 2, - maxClusters = 7, - init = True - ).train(sampled_pts) + no_sampled_pts = sampled_pts.size() + + def if_enough_sample_pts(im): + clusterer = ee.Clusterer.wekaCascadeKMeans( + minClusters = 2, + maxClusters = 7, + init = True + ).train(sampled_pts) + + classified = im.select(band_subset).cluster(clusterer) + im = im.addBands(classified) + max_cluster_value = ee.Number(im.select('cluster').reduceRegion( + reducer = ee.Reducer.max(), + geometry = aoi, + scale = LARGE_SCALE, + maxPixels = 1e10 + ).get('cluster')) + return ee.Dictionary({'max_cluster_value': max_cluster_value, 'classified': classified}) + + def if_not_enough_sample_pts(): + return ee.Dictionary({'max_cluster_value': ee.Number(0), 'classified': ee.Image(0).rename('cluster')}) + + # If clustering is possible do clustering + def if_clustering_possible(max_cluster_value,classified,im): + im = im.addBands(classified) + + water_cluster = identify_water_cluster(im, max_cluster_value) + water_map = classified.select('cluster').eq(ee.Image.constant(water_cluster)).select(['cluster'], ['water_map_cordeiro']) + return water_map - # Classify clusters - classified = im.select(band_subset).cluster(clusterer) - im = im.addBands(classified) + # If no clustering is possible, use NDWI water map + def if_clustering_not_possible(im): + water_map = im.select(['water_map_NDWI'],['water_map_cordeiro']) + return water_map - # Select cluster with highest average MBWI and say it as water map cordeiro - water_cluster = identify_water_cluster(im) - water_map = classified.select('cluster').eq(ee.Image.constant(water_cluster)).select(['cluster'], ['water_map_cordeiro']) + + after_training_dict = ee.Dictionary(ee.Algorithms.If(ee.Number(no_sampled_pts), + if_enough_sample_pts(im), + if_not_enough_sample_pts())) + + max_cluster_value = ee.Number(ee.Algorithms.If(ee.Algorithms.IsEqual( + ee.Number(after_training_dict.get('max_cluster_value'))), + ee.Number(0), + ee.Number(after_training_dict.get('max_cluster_value')))) + + classified = ee.Image(after_training_dict.get('classified')) + water_map = ee.Image(ee.Algorithms.If(ee.Algorithms.IsEqual(max_cluster_value,ee.Number(0)), + if_clustering_not_possible(im), + if_clustering_possible(max_cluster_value, + classified, + im) + )) im = im.addBands(water_map) return im @@ -146,7 +183,8 @@ def process_image(im): cloud_percent = cloud_area.multiply(100).divide(aoi.area()) cordeiro_will_run_when = cloud_percent.lt(CLOUD_COVER_LIMIT) - + # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. + im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) # Clustering based classification of water pixels for cloud pixels when cloud cover is less than CLOUD_COVER_LIMIT im = im.addBands(ee.Image(ee.Algorithms.If(cordeiro_will_run_when, cordeiro(im), ee.Image.constant(-1e6)))) # run cordeiro only if cloud percent is < 90% @@ -171,8 +209,6 @@ def process_image(im): ee.Number(-1e6) )) - # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. - im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) # Calculate water area in water_map_NDWI. water_area_NDWI = ee.Number(im.select('water_map_NDWI').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), @@ -256,27 +292,44 @@ def postprocess(): maxPixels = 1e10 ).get('occurrence')) - counts = ee.Array(hist).transpose().toList() - - omega = ee.Number(0.17) - count_thresh = ee.Number(counts.map(lambda lis: ee.List(lis).reduce(ee.Reducer.mean())).get(1)).multiply(omega) + def if_hist_not_null(im,hist): + + counts = ee.Array(hist).transpose().toList() + + omega = ee.Number(0.17) + count_thresh = ee.Number(counts.map(lambda lis: ee.List(lis).reduce(ee.Reducer.mean())).get(1)).multiply(omega) + + count_thresh_index = ee.Array(counts.get(1)).gt(count_thresh).toList().indexOf(1) + occurrence_thresh = ee.Number(ee.List(counts.get(0)).get(count_thresh_index)) + + water_map = im.select([bandName], ['water_map']) + gswd_improvement = gswd.clip(aoi).gte(occurrence_thresh).updateMask(water_map.mask().Not()).select(["occurrence"], ["water_map"]) + + improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) + + corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_zhao_gao')) + + improved = improved.set("corrected_area", corrected_area.multiply(1e-6)); + improved = improved.set("POSTPROCESSING_SUCCESSFUL", 1); - count_thresh_index = ee.Array(counts.get(1)).gt(count_thresh).toList().indexOf(1) - occurrence_thresh = ee.Number(ee.List(counts.get(0)).get(count_thresh_index)) - - water_map = im.select([bandName], ['water_map']) - gswd_improvement = gswd.clip(aoi).gte(occurrence_thresh).updateMask(water_map.mask().Not()).select(["occurrence"], ["water_map"]) + return improved - improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) + def if_hist_null(im): + # Preserve the uncorrected area in the corrected area column & POSTPROCESSING=1 + # because otherwise it will be substituted with nan. + uncorrected_area = ee.Number(im.get('water_area_clustering')) + improved = im.set('corrected_area', uncorrected_area) + improved = improved.set('POSTPROCESSING_SUCCESSFUL', 1) + return improved - corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( - reducer = ee.Reducer.sum(), - geometry = aoi, - scale = SMALL_SCALE, - maxPixels = 1e10 - ).get('water_map_zhao_gao')) - - improved = improved.set("corrected_area", corrected_area.multiply(1e-6)) + improved = ee.Image(ee.Algorithms.If( + hist, if_hist_not_null(im,hist), if_hist_null(im) + )) return improved def dont_post_process(): @@ -501,3 +554,18 @@ def run_process_long(res_name, res_polygon, start, end, datadir): # User-facing wrapper function def sarea_l7(res_name,res_polygon, start, end, datadir): return run_process_long(res_name,res_polygon, start, end, datadir) + +def grouper(iterable, n, *, incomplete='fill', fillvalue=None): + "Collect data into non-overlapping fixed-length chunks or blocks" + # grouper('ABCDEFG', 3, fillvalue='x') --> ABC DEF Gxx + # grouper('ABCDEFG', 3, incomplete='strict') --> ABC DEF ValueError + # grouper('ABCDEFG', 3, incomplete='ignore') --> ABC DEF + args = [iter(iterable)] * n + if incomplete == 'fill': + return zip_longest(*args, fillvalue=fillvalue) + if incomplete == 'strict': + return zip(*args, strict=True) + if incomplete == 'ignore': + return zip(*args) + else: + raise ValueError('Expected fill, strict, or ignore') \ No newline at end of file diff --git a/src/rat/core/sarea/sarea_cli_l8.py b/src/rat/core/sarea/sarea_cli_l8.py index 0125396f..43e75657 100644 --- a/src/rat/core/sarea/sarea_cli_l8.py +++ b/src/rat/core/sarea/sarea_cli_l8.py @@ -98,17 +98,10 @@ def preprocess(im): ## Processing individual images - water classification ## ##########################################################/ -def identify_water_cluster(im): +def identify_water_cluster(im, max_cluster_value): im = ee.Image(im) mbwi = im.select('MBWI') - - max_cluster_value = ee.Number(im.select('cluster').reduceRegion( - reducer = ee.Reducer.max(), - geometry = aoi, - scale = LARGE_SCALE, - maxPixels = 1e10 - ).get('cluster')) - + clusters = ee.List.sequence(0, max_cluster_value) def calc_avg_mbwi(cluster_val): @@ -119,8 +112,11 @@ def calc_avg_mbwi(cluster_val): geometry = aoi, maxPixels = 1e10 ).get('MBWI') - return avg_mbwi - + avg_mbwi_not_null = ee.Number(ee.Algorithms.If(ee.Algorithms.IsEqual(ee.Number(avg_mbwi)), + ee.Number(-99), + ee.Number(avg_mbwi))) + return avg_mbwi_not_null + # print(calc_avg_mbwi(clusters.get(0)).getInfo()) avg_mbwis = ee.Array(clusters.map(calc_avg_mbwi)) max_mbwi_index = avg_mbwis.argmax().get(0) @@ -129,29 +125,69 @@ def calc_avg_mbwi(cluster_val): return water_cluster - def cordeiro(im): - band_subset = ee.List(['NDWI', 'MNDWI', 'SR_B7']) # using NDWI, MNDWI and B7 (SWIR) + ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on + ## calinski harabasz's work + ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans + band_subset = ee.List(['NDWI', 'MNDWI', SWIR2_BAND_NAME]) # using NDWI, MNDWI and B7 (SWIR2) sampled_pts = im.select(band_subset).sample( region = aoi, scale = SMALL_SCALE, numPixels = 5e3-1 ## limit of 5k points ) - ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on - ## calinski harabasz's work - ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans - clusterer = ee.Clusterer.wekaCascadeKMeans( - minClusters = 2, - maxClusters = 7, - init = True - ).train(sampled_pts) + no_sampled_pts = sampled_pts.size() + + def if_enough_sample_pts(im): + clusterer = ee.Clusterer.wekaCascadeKMeans( + minClusters = 2, + maxClusters = 7, + init = True + ).train(sampled_pts) + + classified = im.select(band_subset).cluster(clusterer) + im = im.addBands(classified) + max_cluster_value = ee.Number(im.select('cluster').reduceRegion( + reducer = ee.Reducer.max(), + geometry = aoi, + scale = LARGE_SCALE, + maxPixels = 1e10 + ).get('cluster')) + return ee.Dictionary({'max_cluster_value': max_cluster_value, 'classified': classified}) + + def if_not_enough_sample_pts(): + return ee.Dictionary({'max_cluster_value': ee.Number(0), 'classified': ee.Image(0).rename('cluster')}) + + # If clustering is possible do clustering + def if_clustering_possible(max_cluster_value,classified,im): + im = im.addBands(classified) + + water_cluster = identify_water_cluster(im, max_cluster_value) + water_map = classified.select('cluster').eq(ee.Image.constant(water_cluster)).select(['cluster'], ['water_map_cordeiro']) + return water_map - classified = im.select(band_subset).cluster(clusterer) - im = im.addBands(classified) + # If no clustering is possible, use NDWI water map + def if_clustering_not_possible(im): + water_map = im.select(['water_map_NDWI'],['water_map_cordeiro']) + return water_map - water_cluster = identify_water_cluster(im) - water_map = classified.select('cluster').eq(ee.Image.constant(water_cluster)).select(['cluster'], ['water_map_cordeiro']) + + after_training_dict = ee.Dictionary(ee.Algorithms.If(ee.Number(no_sampled_pts), + if_enough_sample_pts(im), + if_not_enough_sample_pts())) + + max_cluster_value = ee.Number(ee.Algorithms.If(ee.Algorithms.IsEqual( + ee.Number(after_training_dict.get('max_cluster_value'))), + ee.Number(0), + ee.Number(after_training_dict.get('max_cluster_value')))) + + classified = ee.Image(after_training_dict.get('classified')) + water_map = ee.Image(ee.Algorithms.If(ee.Algorithms.IsEqual(max_cluster_value,ee.Number(0)), + if_clustering_not_possible(im), + if_clustering_possible(max_cluster_value, + classified, + im) + )) im = im.addBands(water_map) return im @@ -182,7 +218,8 @@ def process_image(im): cloud_percent = cloud_area.multiply(100).divide(aoi.area()) cordeiro_will_run_when = cloud_percent.lt(CLOUD_COVER_LIMIT) - + # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. + im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) # Clusting based im = im.addBands(ee.Image(ee.Algorithms.If(cordeiro_will_run_when, cordeiro(im), ee.Image.constant(-1e6)))) # run cordeiro only if cloud percent is < 90% @@ -205,8 +242,7 @@ def process_image(im): ee.Number(-1e6) )) - # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. - im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) + water_area_NDWI = ee.Number(im.select('water_map_NDWI').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, @@ -287,28 +323,46 @@ def postprocess(): maxPixels = 1e10 ).get('occurrence')) - counts = ee.Array(hist).transpose().toList() - - omega = ee.Number(0.17) - count_thresh = ee.Number(counts.map(lambda lis: ee.List(lis).reduce(ee.Reducer.mean())).get(1)).multiply(omega) + def if_hist_not_null(im,hist): + + counts = ee.Array(hist).transpose().toList() + + omega = ee.Number(0.17) + count_thresh = ee.Number(counts.map(lambda lis: ee.List(lis).reduce(ee.Reducer.mean())).get(1)).multiply(omega) + + count_thresh_index = ee.Array(counts.get(1)).gt(count_thresh).toList().indexOf(1) + occurrence_thresh = ee.Number(ee.List(counts.get(0)).get(count_thresh_index)) + + water_map = im.select([bandName], ['water_map']) + gswd_improvement = gswd.clip(aoi).gte(occurrence_thresh).updateMask(water_map.mask().Not()).select(["occurrence"], ["water_map"]) + + improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) + + corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_zhao_gao')) + + improved = improved.set("corrected_area", corrected_area.multiply(1e-6)); + improved = improved.set("POSTPROCESSING_SUCCESSFUL", 1); - count_thresh_index = ee.Array(counts.get(1)).gt(count_thresh).toList().indexOf(1) - occurrence_thresh = ee.Number(ee.List(counts.get(0)).get(count_thresh_index)) - - water_map = im.select([bandName], ['water_map']) - gswd_improvement = gswd.clip(aoi).gte(occurrence_thresh).updateMask(water_map.mask().Not()).select(["occurrence"], ["water_map"]) + return improved - improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) + def if_hist_null(im): + # Preserve the uncorrected area in the corrected area column & POSTPROCESSING=1 + # because otherwise it will be substituted with nan. + uncorrected_area = ee.Number(im.get('water_area_clustering')) + improved = im.set('corrected_area', uncorrected_area) + improved = improved.set('POSTPROCESSING_SUCCESSFUL', 1) + return improved - corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( - reducer = ee.Reducer.sum(), - geometry = aoi, - scale = SMALL_SCALE, - maxPixels = 1e10 - ).get('water_map_zhao_gao')) - - improved = improved.set("corrected_area", corrected_area.multiply(1e-6)) + improved = ee.Image(ee.Algorithms.If( + hist, if_hist_not_null(im,hist), if_hist_null(im) + )) return improved + def dont_post_process(): improved = ee.Image.constant(-1) diff --git a/src/rat/core/sarea/sarea_cli_l9.py b/src/rat/core/sarea/sarea_cli_l9.py index 1ac6a4b8..95313b05 100644 --- a/src/rat/core/sarea/sarea_cli_l9.py +++ b/src/rat/core/sarea/sarea_cli_l9.py @@ -101,17 +101,10 @@ def preprocess(im): ## Processing individual images - water classification ## ##########################################################/ -def identify_water_cluster(im): +def identify_water_cluster(im, max_cluster_value): im = ee.Image(im) mbwi = im.select('MBWI') - - max_cluster_value = ee.Number(im.select('cluster').reduceRegion( - reducer = ee.Reducer.max(), - geometry = aoi, - scale = LARGE_SCALE, - maxPixels = 1e10 - ).get('cluster')) - + clusters = ee.List.sequence(0, max_cluster_value) def calc_avg_mbwi(cluster_val): @@ -122,8 +115,11 @@ def calc_avg_mbwi(cluster_val): geometry = aoi, maxPixels = 1e10 ).get('MBWI') - return avg_mbwi - + avg_mbwi_not_null = ee.Number(ee.Algorithms.If(ee.Algorithms.IsEqual(ee.Number(avg_mbwi)), + ee.Number(-99), + ee.Number(avg_mbwi))) + return avg_mbwi_not_null + # print(calc_avg_mbwi(clusters.get(0)).getInfo()) avg_mbwis = ee.Array(clusters.map(calc_avg_mbwi)) max_mbwi_index = avg_mbwis.argmax().get(0) @@ -134,27 +130,68 @@ def calc_avg_mbwi(cluster_val): def cordeiro(im): - band_subset = ee.List(['NDWI', 'MNDWI', 'SR_B7']) # using NDWI, MNDWI and B7 (SWIR) + ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on + ## calinski harabasz's work + ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans + band_subset = ee.List(['NDWI', 'MNDWI', SWIR2_BAND_NAME]) # using NDWI, MNDWI and B7 (SWIR2) sampled_pts = im.select(band_subset).sample( region = aoi, scale = SMALL_SCALE, numPixels = 5e3-1 ## limit of 5k points ) - ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on - ## calinski harabasz's work - ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans - clusterer = ee.Clusterer.wekaCascadeKMeans( - minClusters = 2, - maxClusters = 7, - init = True - ).train(sampled_pts) + no_sampled_pts = sampled_pts.size() + + def if_enough_sample_pts(im): + clusterer = ee.Clusterer.wekaCascadeKMeans( + minClusters = 2, + maxClusters = 7, + init = True + ).train(sampled_pts) + + classified = im.select(band_subset).cluster(clusterer) + im = im.addBands(classified) + max_cluster_value = ee.Number(im.select('cluster').reduceRegion( + reducer = ee.Reducer.max(), + geometry = aoi, + scale = LARGE_SCALE, + maxPixels = 1e10 + ).get('cluster')) + return ee.Dictionary({'max_cluster_value': max_cluster_value, 'classified': classified}) + + def if_not_enough_sample_pts(): + return ee.Dictionary({'max_cluster_value': ee.Number(0), 'classified': ee.Image(0).rename('cluster')}) + + # If clustering is possible do clustering + def if_clustering_possible(max_cluster_value,classified,im): + im = im.addBands(classified) + + water_cluster = identify_water_cluster(im, max_cluster_value) + water_map = classified.select('cluster').eq(ee.Image.constant(water_cluster)).select(['cluster'], ['water_map_cordeiro']) + return water_map - classified = im.select(band_subset).cluster(clusterer) - im = im.addBands(classified) + # If no clustering is possible, use NDWI water map + def if_clustering_not_possible(im): + water_map = im.select(['water_map_NDWI'],['water_map_cordeiro']) + return water_map - water_cluster = identify_water_cluster(im) - water_map = classified.select('cluster').eq(ee.Image.constant(water_cluster)).select(['cluster'], ['water_map_cordeiro']) + + after_training_dict = ee.Dictionary(ee.Algorithms.If(ee.Number(no_sampled_pts), + if_enough_sample_pts(im), + if_not_enough_sample_pts())) + + max_cluster_value = ee.Number(ee.Algorithms.If(ee.Algorithms.IsEqual( + ee.Number(after_training_dict.get('max_cluster_value'))), + ee.Number(0), + ee.Number(after_training_dict.get('max_cluster_value')))) + + classified = ee.Image(after_training_dict.get('classified')) + water_map = ee.Image(ee.Algorithms.If(ee.Algorithms.IsEqual(max_cluster_value,ee.Number(0)), + if_clustering_not_possible(im), + if_clustering_possible(max_cluster_value, + classified, + im) + )) im = im.addBands(water_map) return im @@ -185,7 +222,8 @@ def process_image(im): cloud_percent = cloud_area.multiply(100).divide(aoi.area()) cordeiro_will_run_when = cloud_percent.lt(CLOUD_COVER_LIMIT) - + # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. + im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) # Clusting based im = im.addBands(ee.Image(ee.Algorithms.If(cordeiro_will_run_when, cordeiro(im), ee.Image.constant(-1e6)))) # run cordeiro only if cloud percent is < 90% @@ -208,8 +246,6 @@ def process_image(im): ee.Number(-1e6) )) - # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. - im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) water_area_NDWI = ee.Number(im.select('water_map_NDWI').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, @@ -290,27 +326,44 @@ def postprocess(): maxPixels = 1e10 ).get('occurrence')) - counts = ee.Array(hist).transpose().toList() - - omega = ee.Number(0.17) - count_thresh = ee.Number(counts.map(lambda lis: ee.List(lis).reduce(ee.Reducer.mean())).get(1)).multiply(omega) - - count_thresh_index = ee.Array(counts.get(1)).gt(count_thresh).toList().indexOf(1) - occurrence_thresh = ee.Number(ee.List(counts.get(0)).get(count_thresh_index)) - - water_map = im.select([bandName], ['water_map']) - gswd_improvement = gswd.clip(aoi).gte(occurrence_thresh).updateMask(water_map.mask().Not()).select(["occurrence"], ["water_map"]) + def if_hist_not_null(im,hist): + + counts = ee.Array(hist).transpose().toList() + + omega = ee.Number(0.17) + count_thresh = ee.Number(counts.map(lambda lis: ee.List(lis).reduce(ee.Reducer.mean())).get(1)).multiply(omega) + + count_thresh_index = ee.Array(counts.get(1)).gt(count_thresh).toList().indexOf(1) + occurrence_thresh = ee.Number(ee.List(counts.get(0)).get(count_thresh_index)) + + water_map = im.select([bandName], ['water_map']) + gswd_improvement = gswd.clip(aoi).gte(occurrence_thresh).updateMask(water_map.mask().Not()).select(["occurrence"], ["water_map"]) + + improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) + + corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_zhao_gao')) + + improved = improved.set("corrected_area", corrected_area.multiply(1e-6)); + improved = improved.set("POSTPROCESSING_SUCCESSFUL", 1); - improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) + return improved - corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( - reducer = ee.Reducer.sum(), - geometry = aoi, - scale = SMALL_SCALE, - maxPixels = 1e10 - ).get('water_map_zhao_gao')) + def if_hist_null(im): + # Preserve the uncorrected area in the corrected area column & POSTPROCESSING=1 + # because otherwise it will be substituted with nan. + uncorrected_area = ee.Number(im.get('water_area_clustering')) + improved = im.set('corrected_area', uncorrected_area) + improved = improved.set('POSTPROCESSING_SUCCESSFUL', 1) + return improved - improved = improved.set("corrected_area", corrected_area.multiply(1e-6)) + improved = ee.Image(ee.Algorithms.If( + hist, if_hist_not_null(im,hist), if_hist_null(im) + )) return improved def dont_post_process(): From 537a20161df71b5f52e921591970bc3b487e1abd Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sat, 21 Dec 2024 18:05:03 -0800 Subject: [PATCH 021/102] handled error handling for small reservoirs when clustering or zhao-gao correction is not possible for sentinel2 --- src/rat/core/sarea/sarea_cli_s2.py | 172 ++++++++++++++++++++--------- 1 file changed, 117 insertions(+), 55 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 8039e813..12186066 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -94,17 +94,10 @@ def preprocess(im): return im -def identify_water_cluster(im): +def identify_water_cluster(im, max_cluster_value): im = ee.Image(im) mbwi = im.select('MBWI') - - max_cluster_value = ee.Number(im.select('cluster').reduceRegion( - reducer = ee.Reducer.max(), - geometry = aoi, - scale = LARGE_SCALE, - maxPixels = 1e10 - ).get('cluster')) - + clusters = ee.List.sequence(0, max_cluster_value) def calc_avg_mbwi(cluster_val): @@ -115,8 +108,11 @@ def calc_avg_mbwi(cluster_val): geometry = aoi, maxPixels = 1e10 ).get('MBWI') - return avg_mbwi - + avg_mbwi_not_null = ee.Number(ee.Algorithms.If(ee.Algorithms.IsEqual(ee.Number(avg_mbwi)), + ee.Number(-99), + ee.Number(avg_mbwi))) + return avg_mbwi_not_null + # print(calc_avg_mbwi(clusters.get(0)).getInfo()) avg_mbwis = ee.Array(clusters.map(calc_avg_mbwi)) max_mbwi_index = avg_mbwis.argmax().get(0) @@ -127,27 +123,67 @@ def calc_avg_mbwi(cluster_val): def clustering(im): + ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on + ## calinski harabasz's work + ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans band_subset = ee.List(['NDWI', 'B12']) sampled_pts = im.select(band_subset).sample( region = aoi, scale = SMALL_SCALE, numPixels = 4999 ## limit of 5k points, staying at 4k ) + no_sampled_pts = sampled_pts.size() + + def if_enough_sample_pts(im): + clusterer = ee.Clusterer.wekaCascadeKMeans( + minClusters = 2, + maxClusters = 7, + init = True + ).train(sampled_pts) + + classified = im.select(band_subset).cluster(clusterer) + im = im.addBands(classified) + max_cluster_value = ee.Number(im.select('cluster').reduceRegion( + reducer = ee.Reducer.max(), + geometry = aoi, + scale = LARGE_SCALE, + maxPixels = 1e10 + ).get('cluster')) + return ee.Dictionary({'max_cluster_value': max_cluster_value, 'classified': classified}) + + def if_not_enough_sample_pts(): + return ee.Dictionary({'max_cluster_value': ee.Number(0), 'classified': ee.Image(0).rename('cluster')}) + + # If clustering is possible do clustering + def if_clustering_possible(max_cluster_value,classified,im): + im = im.addBands(classified) + + water_cluster = identify_water_cluster(im, max_cluster_value) + water_map = classified.select('cluster').eq(ee.Image.constant(water_cluster)).select(['cluster'], ['water_map_clustering']) + return water_map + + # If no clustering is possible, use NDWI water map + def if_clustering_not_possible(im): + water_map = im.select(['water_map_NDWI'],['water_map_clustering']) + return water_map - ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on - ## calinski harabasz's work - ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans - clusterer = ee.Clusterer.wekaCascadeKMeans( - minClusters = 2, - maxClusters = 7, - init = True - ).train(sampled_pts) - classified = im.select(band_subset).cluster(clusterer) - im = im.addBands(classified) + after_training_dict = ee.Dictionary(ee.Algorithms.If(ee.Number(no_sampled_pts), + if_enough_sample_pts(im), + if_not_enough_sample_pts())) - water_cluster = identify_water_cluster(im) - water_map = classified.select('cluster').eq(ee.Image.constant(water_cluster)).select(['cluster'], ['water_map_clustering']) + max_cluster_value = ee.Number(ee.Algorithms.If(ee.Algorithms.IsEqual( + ee.Number(after_training_dict.get('max_cluster_value'))), + ee.Number(0), + ee.Number(after_training_dict.get('max_cluster_value')))) + + classified = ee.Image(after_training_dict.get('classified')) + water_map = ee.Image(ee.Algorithms.If(ee.Algorithms.IsEqual(max_cluster_value,ee.Number(0)), + if_clustering_not_possible(im), + if_clustering_possible(max_cluster_value, + classified, + im) + )) im = im.addBands(water_map) return im @@ -169,6 +205,9 @@ def process_image(im): }) im = im.addBands(mbwi); + # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. + im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) + cloud_area = aoi.area().subtract(im.select('cloud').Not().multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, @@ -178,8 +217,9 @@ def process_image(im): cloud_percent = cloud_area.multiply(100).divide(aoi.area()) CLOUD_LIMIT_SATISFIED = cloud_percent.lt(CLOUD_COVER_LIMIT) - + # Clustering based + # print('starting clustering') im = im.addBands( ee.Image( ee.Algorithms.If( @@ -189,7 +229,9 @@ def process_image(im): ) ) ) # run clustering only if cloud percent is < 90% - + # except: + # print('Clustering could not be done. Using NDWI water map instead.') + # water_map = 'water_map_NDWI' water_area_clustering = ee.Number( ee.Algorithms.If( CLOUD_LIMIT_SATISFIED, @@ -305,45 +347,66 @@ def postprocess(im, bandName='water_map_clustering'): maxPixels = 1e10 ).get('occurrence')) - counts = ee.Array(hist).transpose().toList() + def if_hist_not_null(im,hist): - omega = ee.Number(0.17) - count_thresh = ee.Number(counts.map(lambda lis: ee.List(lis).reduce(ee.Reducer.mean())).get(1)).multiply(omega) - - count_thresh_index = ee.Array(counts.get(1)).gt(count_thresh).toList().indexOf(1) - occurrence_thresh = ee.Number(ee.List(counts.get(0)).get(count_thresh_index)) + counts = ee.Array(hist).transpose().toList() + + omega = ee.Number(0.17) + count_thresh = ee.Number(counts.map(lambda lis: ee.List(lis).reduce(ee.Reducer.mean())).get(1)).multiply(omega) + + count_thresh_index = ee.Array(counts.get(1)).gt(count_thresh).toList().indexOf(1) + occurrence_thresh = ee.Number(ee.List(counts.get(0)).get(count_thresh_index)) - water_map = im.select([bandName], ['water_map']) - gswd_improvement = gswd.clip(aoi).gte(occurrence_thresh).updateMask(water_map.mask().Not()).select(["occurrence"], ["water_map"]) - - improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) + water_map = im.select([bandName], ['water_map']) + gswd_improvement = gswd.clip(aoi).gte(occurrence_thresh).updateMask(water_map.mask().Not()).select(["occurrence"], ["water_map"]) + + improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) + + corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( + reducer = ee.Reducer.sum(), + geometry = aoi, + scale = SMALL_SCALE, + maxPixels = 1e10 + ).get('water_map_zhao_gao')) + + improved = improved.set("corrected_area", corrected_area.multiply(1e-6)); + improved = improved.set("POSTPROCESSING_SUCCESSFUL", 1); - corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( - reducer = ee.Reducer.sum(), - geometry = aoi, - scale = SMALL_SCALE, - maxPixels = 1e10 - ).get('water_map_zhao_gao')) + return improved - improved = improved.set("corrected_area", corrected_area.multiply(1e-6)); - improved = improved.set("POSTPROCESSING_SUCCESSFUL", 1); + def if_hist_null(im): + # Preserve the uncorrected area in the corrected area column & POSTPROCESSING=1 + # because otherwise it will be substituted with nan. + uncorrected_area = ee.Number(im.get('water_area_clustering')) + improved = im.set('corrected_area', uncorrected_area) + improved = improved.set('POSTPROCESSING_SUCCESSFUL', 1) + return improved + improved = ee.Image(ee.Algorithms.If( + hist, if_hist_not_null(im,hist), if_hist_null(im) + )) return improved - -def postprocess_wrapper(im, bandName='water_map_clustering'): - def do_not_postprocess(): - im = ee.Image.constant(-1) - im = im.set('corrected_area', -1) - im = im.set('POSTPROCESSING_SUCCESSFUL', 0) +def postprocess_wrapper(im, bandName='water_map_clustering'): - return im + def do_not_postprocess(): + default_im = ee.Image.constant(-1).rename(bandName) + default_im = default_im.set('corrected_area', -1) + default_im = default_im.set('POSTPROCESSING_SUCCESSFUL', 0) + return default_im - improved = ee.Algorithms.If( + # Ensure PROCESSING_SUCCESSFUL is boolean and defaults to False + processing_successful = ee.Algorithms.If( + im.get('PROCESSING_SUCCESSFUL'), im.get('PROCESSING_SUCCESSFUL'), + False + ) + + improved = ee.Image(ee.Algorithms.If( + processing_successful, postprocess(im, bandName), do_not_postprocess() - ) + )) return improved @@ -382,7 +445,7 @@ def not_enough_images(): not_enough_images() ) ) - + # print('Processed image') im = im.set('from_date', from_date.format("YYYY-MM-dd")) im = im.set('to_date', to_date.format("YYYY-MM-dd")) im = im.set('system:time_start', date.format("YYYY-MM-dd")) @@ -393,6 +456,7 @@ def not_enough_images(): def generate_timeseries(dates): + # raw_ts = process_date(dates.get(4)) raw_ts = dates.map(process_date) # raw_ts = raw_ts.removeAll([0]); imcoll = ee.ImageCollection.fromImages(raw_ts) @@ -474,11 +538,9 @@ def run_process_long(res_name,res_polygon, start, end, datadir): ts_imcoll = generate_timeseries(dates) postprocessed_ts_imcoll = ts_imcoll.map(postprocess_wrapper) - # Download the data locally ts_imcoll_L = ts_imcoll.getInfo() postprocessed_ts_imcoll_L = postprocessed_ts_imcoll.getInfo() - # Parse the data to create dataframe PROCESSING_STATUSES = [] POSTPROCESSING_STATUSES = [] From 4be8653b452237956af9f385622737b7c5dd5e35 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sat, 21 Dec 2024 18:08:23 -0800 Subject: [PATCH 022/102] Bug Fix: Stderr was missing & Exception was not correctly coded. --- src/rat/run_rat.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/rat/run_rat.py b/src/rat/run_rat.py index 42c435b2..856e87ad 100644 --- a/src/rat/run_rat.py +++ b/src/rat/run_rat.py @@ -3,6 +3,7 @@ import datetime import copy import os +import sys import pandas as pd import numpy as np @@ -69,6 +70,9 @@ def run_rat(config_fn, operational_latency=None ): log.info("Connected to earth engine succesfully.") except Exception as e: log.error(f"Failed to connect to Earth Engine. RAT will not be able to use Surface Area Estimations. Error: {e}") + finally: + # Ensure sys.stderr is restored + sys.stderr = sys.__stderr__ ############################ ----------- Single basin run ---------------- ###################################### if(not config['GLOBAL']['multiple_basin_run']): @@ -163,7 +167,7 @@ def run_rat(config_fn, operational_latency=None ): try: basins_metadata = pd.read_csv(config['GLOBAL']['basins_metadata'],header=[0,1]) except: - raise("Please provide the proper path of a csv file in basins_metadata in the Global section of RAT's config file") + raise Exception("Please provide the proper path of a csv file in basins_metadata in the Global section of RAT's config file") if ('BASIN','run') in basins_metadata.columns: basins_metadata_filtered = basins_metadata[basins_metadata['BASIN','run']==1] ####### Remove in future version : Deprecation (start)######## @@ -172,13 +176,13 @@ def run_rat(config_fn, operational_latency=None ): if ('BASIN','basin_name') in basins_metadata.columns: basins_metadata_filtered = basins_metadata[basins_metadata['BASIN','basin_name'].isin(config['GLOBAL']['basins_to_process'])] else: - raise("No column in 'basins_metadata' file corresponding to 'basin_name' in 'BASIN' section of RAT's config file.") + raise Exception("No column in 'basins_metadata' file corresponding to 'basin_name' in 'BASIN' section of RAT's config file.") ####### Remove in future version : Deprecation (end) ######## if ('BASIN','basin_name') in basins_metadata.columns: basins_to_process = basins_metadata_filtered['BASIN','basin_name'].tolist() else: - raise("No column in 'basins_metadata' file corresponding to 'basin_name' in 'BASIN' section of RAT's config file.") + raise Exception("No column in 'basins_metadata' file corresponding to 'basin_name' in 'BASIN' section of RAT's config file.") # For each basin for basin in basins_to_process: From e2b0c9baadea6ae411e513262445a8aecd3b9ba3 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sat, 21 Dec 2024 19:03:59 -0800 Subject: [PATCH 023/102] removed dupli --- src/rat/core/sarea/sarea_cli_s2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 12186066..608ea656 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -7,7 +7,6 @@ import os from random import randint from itertools import zip_longest -from rat.ee_utils.ee_utils import poly2feature from rat.ee_utils.ee_utils import poly2feature from rat.utils.logging import LOG_NAME, NOTIFICATION From 63ebadf50ce195139fcb0aee0fc9d4f85e49730f Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Tue, 24 Dec 2024 16:43:59 -0800 Subject: [PATCH 024/102] Handling of error "too many concurrent aggregations" for larger reservoirs --- src/rat/core/sarea/sarea_cli_s2.py | 219 ++++++++++++++++------------- 1 file changed, 124 insertions(+), 95 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 608ea656..e35202e8 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -45,6 +45,7 @@ def grouper(iterable, n, *, incomplete='fill', fillvalue=None): end_date = ee.Date('2019-02-01') TEMPORAL_RESOLUTION = 5 RESULTS_PER_ITER = 5 +MIN_RESULTS_PER_ITER = 1 MISSION_START_DATE = (2022,1,1) # Rough start date for mission/satellite data QUALITY_PIXEL_BAND_NAME = 'QA_PIXEL' BLUE_BAND_NAME = 'B2' @@ -467,7 +468,7 @@ def get_first_obs(start_date, end_date): str_fmt = 'YYYY-MM-dd' return ee.Date.parse(str_fmt, ee.Date(first_im.get('system:time_start')).format(str_fmt)) -def run_process_long(res_name,res_polygon, start, end, datadir): +def run_process_long(res_name,res_polygon, start, end, datadir, results_per_iter=RESULTS_PER_ITER): fo = start enddate = end @@ -514,7 +515,7 @@ def run_process_long(res_name,res_polygon, start, end, datadir): print(f"Extracting SA for the period {fo} -> {enddate}") dates = pd.date_range(fo, enddate, freq=f'{TEMPORAL_RESOLUTION}D') - grouped_dates = grouper(dates, RESULTS_PER_ITER) + grouped_dates = grouper(dates, results_per_iter) # # redo the calculations part and see where it is complaining about too many aggregations # subset_dates = next(grouped_dates) @@ -528,102 +529,130 @@ def run_process_long(res_name,res_polygon, start, end, datadir): # uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') # uncorrected_final_data = uncorrected_final_data_ee.getInfo() - - - for subset_dates in grouped_dates: + # Until results per iteration is less than min results per iteration + while results_per_iter >= MIN_RESULTS_PER_ITER: + # try to run for each subset of dates try: - print(subset_dates) - dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) - - ts_imcoll = generate_timeseries(dates) - postprocessed_ts_imcoll = ts_imcoll.map(postprocess_wrapper) - # Download the data locally - ts_imcoll_L = ts_imcoll.getInfo() - postprocessed_ts_imcoll_L = postprocessed_ts_imcoll.getInfo() - # Parse the data to create dataframe - PROCESSING_STATUSES = [] - POSTPROCESSING_STATUSES = [] - cloud_areas = [] - cloud_percents = [] - from_dates = [] - to_dates = [] - obs_dates = [] - non_water_areas = [] - water_areas = [] - water_areas_zhaogao = [] - water_red_sums = [] - water_green_sums = [] - water_nir_sums = [] - water_red_green_means = [] - water_nir_red_means = [] - for f, f_postprocessed in zip(ts_imcoll_L['features'], postprocessed_ts_imcoll_L['features']): - PROCESSING_STATUS = f['properties']['PROCESSING_SUCCESSFUL'] - PROCESSING_STATUSES.append(PROCESSING_STATUS) - POSTPROCESSING_STATUS = f_postprocessed['properties']['POSTPROCESSING_SUCCESSFUL'] - POSTPROCESSING_STATUSES.append(POSTPROCESSING_STATUS) - obs_dates.append(pd.to_datetime(f['properties']['system:time_start'])) - from_dates.append(pd.to_datetime(f['properties']['from_date'])) - to_dates.append(pd.to_datetime(f['properties']['to_date'])) - if PROCESSING_STATUS: - water_areas.append(f['properties']['water_area_clustering']) - non_water_areas.append(f['properties']['non_water_area_clustering']) - cloud_areas.append(f['properties']['cloud_area']) - cloud_percents.append(f['properties']['cloud_percent']) - water_red_sums.append(f['properties']['water_red_sum']) - water_green_sums.append(f['properties']['water_green_sum']) - water_nir_sums.append(f['properties']['water_nir_sum']) - water_red_green_means.append(f['properties']['water_red_green_mean']) - water_nir_red_means.append(f['properties']['water_nir_red_mean']) - else: - water_areas.append(np.nan) - non_water_areas.append(np.nan) - cloud_areas.append(np.nan) - cloud_percents.append(np.nan) - water_red_sums.append(np.nan) - water_green_sums.append(np.nan) - water_nir_sums.append(np.nan) - water_red_green_means.append(np.nan) - water_nir_red_means.append(np.nan) - if POSTPROCESSING_STATUS: - water_areas_zhaogao.append(f_postprocessed['properties']['corrected_area']) - else: - water_areas_zhaogao.append(np.nan) - - df = pd.DataFrame({ - 'date': obs_dates, - 'PROCESSING_STATUS': PROCESSING_STATUSES, - 'POSTPROCESSING_STATUS': POSTPROCESSING_STATUSES, - 'from_date': from_dates, - 'to_date': to_dates, - 'cloud_area': cloud_areas, - 'cloud_percent': cloud_percents, - 'water_area_uncorrected': water_areas, - 'non_water_area': non_water_areas, - 'water_area_corrected': water_areas_zhaogao, - 'water_red_sum': water_red_sums, - 'water_green_sum': water_green_sums, - 'water_nir_sum': water_nir_sums, - 'water_red_green_mean': water_red_green_means, - 'water_nir_red_mean': water_nir_red_means - }).set_index('date') - - fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") - df.to_csv(fname) - print(df.tail()) - - s_time = randint(5, 10) - print(f"Sleeping for {s_time} seconds") - time.sleep(s_time) - - if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: - print(f"Quitting: Reached enddate {enddate}") - break - elif df.index[-1].strftime('%Y-%m-%d') == fo: - print(f"Reached last available observation - {fo}") - break + for subset_dates in grouped_dates: + # try to run for subset_dates with results_per_iter + try: + print(subset_dates) + dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) + + ts_imcoll = generate_timeseries(dates) + postprocessed_ts_imcoll = ts_imcoll.map(postprocess_wrapper) + # Download the data locally + ts_imcoll_L = ts_imcoll.getInfo() + postprocessed_ts_imcoll_L = postprocessed_ts_imcoll.getInfo() + # Parse the data to create dataframe + PROCESSING_STATUSES = [] + POSTPROCESSING_STATUSES = [] + cloud_areas = [] + cloud_percents = [] + from_dates = [] + to_dates = [] + obs_dates = [] + non_water_areas = [] + water_areas = [] + water_areas_zhaogao = [] + water_red_sums = [] + water_green_sums = [] + water_nir_sums = [] + water_red_green_means = [] + water_nir_red_means = [] + for f, f_postprocessed in zip(ts_imcoll_L['features'], postprocessed_ts_imcoll_L['features']): + PROCESSING_STATUS = f['properties']['PROCESSING_SUCCESSFUL'] + PROCESSING_STATUSES.append(PROCESSING_STATUS) + POSTPROCESSING_STATUS = f_postprocessed['properties']['POSTPROCESSING_SUCCESSFUL'] + POSTPROCESSING_STATUSES.append(POSTPROCESSING_STATUS) + obs_dates.append(pd.to_datetime(f['properties']['system:time_start'])) + from_dates.append(pd.to_datetime(f['properties']['from_date'])) + to_dates.append(pd.to_datetime(f['properties']['to_date'])) + if PROCESSING_STATUS: + water_areas.append(f['properties']['water_area_clustering']) + non_water_areas.append(f['properties']['non_water_area_clustering']) + cloud_areas.append(f['properties']['cloud_area']) + cloud_percents.append(f['properties']['cloud_percent']) + water_red_sums.append(f['properties']['water_red_sum']) + water_green_sums.append(f['properties']['water_green_sum']) + water_nir_sums.append(f['properties']['water_nir_sum']) + water_red_green_means.append(f['properties']['water_red_green_mean']) + water_nir_red_means.append(f['properties']['water_nir_red_mean']) + else: + water_areas.append(np.nan) + non_water_areas.append(np.nan) + cloud_areas.append(np.nan) + cloud_percents.append(np.nan) + water_red_sums.append(np.nan) + water_green_sums.append(np.nan) + water_nir_sums.append(np.nan) + water_red_green_means.append(np.nan) + water_nir_red_means.append(np.nan) + if POSTPROCESSING_STATUS: + water_areas_zhaogao.append(f_postprocessed['properties']['corrected_area']) + else: + water_areas_zhaogao.append(np.nan) + + df = pd.DataFrame({ + 'date': obs_dates, + 'PROCESSING_STATUS': PROCESSING_STATUSES, + 'POSTPROCESSING_STATUS': POSTPROCESSING_STATUSES, + 'from_date': from_dates, + 'to_date': to_dates, + 'cloud_area': cloud_areas, + 'cloud_percent': cloud_percents, + 'water_area_uncorrected': water_areas, + 'non_water_area': non_water_areas, + 'water_area_corrected': water_areas_zhaogao, + 'water_red_sum': water_red_sums, + 'water_green_sum': water_green_sums, + 'water_nir_sum': water_nir_sums, + 'water_red_green_mean': water_red_green_means, + 'water_nir_red_mean': water_nir_red_means + }).set_index('date') + + fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") + df.to_csv(fname) + print(df.tail()) + + s_time = randint(5, 10) + print(f"Sleeping for {s_time} seconds") + time.sleep(s_time) + + if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: + print(f"Quitting: Reached enddate {enddate}") + break + elif df.index[-1].strftime('%Y-%m-%d') == fo: + print(f"Reached last available observation - {fo}") + break + # If exception is "Too many concurrent aggregations", reduce results_per_iter + # and rerun for loop for leftover dates by raising Exception. + # Else just print the exception and continue. + except Exception as e: + log.error(e) + # Adjust results_per_iter only if error includes "Too many concurrent aggregations" + if "Too many concurrent aggregations" in str(e): + results_per_iter -= 1 + print(f"Reducing Results per iteration to {results_per_iter} due to error.") + if results_per_iter < MIN_RESULTS_PER_ITER: + print("Minimum Results per iteration reached. Continuing to next group of dates.") + results_per_iter = MIN_RESULTS_PER_ITER + continue + else: + raise Exception(f'Reducing Results per iteration to {results_per_iter}.') + else: + continue + # This exception will be only raised if the error is "Too many concurrent aggregations". + # and Results per iteration will be reduced but still be greater than or equal to minimum results per iteration. + # We will continue while loop and for loop within while loop from the left over grouped dates. except Exception as e: - log.error(e) + dates = pd.date_range(subset_dates[0], enddate, freq=f'{TEMPORAL_RESOLUTION}D') + grouped_dates = grouper(dates, results_per_iter) continue + # In case no exception is raised and the complete for loop ran succesfully, break the while loop + # because we need to run the for loop only once. + else: + break # Combine the files into one database to_combine.extend([os.path.join(savedir, f) for f in os.listdir(savedir) if f.endswith(".csv")]) From 57496a27a1877554d64284df767321a8f1430898 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Tue, 31 Dec 2024 17:10:56 -0800 Subject: [PATCH 025/102] Fixed bug: no gdf --- src/rat/toolbox/data_transform.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/rat/toolbox/data_transform.py b/src/rat/toolbox/data_transform.py index 3c28f53d..eddc419a 100644 --- a/src/rat/toolbox/data_transform.py +++ b/src/rat/toolbox/data_transform.py @@ -83,11 +83,6 @@ def create_meterological_ts(roi, nc_file_path, output_csv_path): print("CRS not found for dataset. Setting CRS to EPSG:4326.") ds.rio.write_crs("EPSG:4326", inplace=True) - # Set CRS for GeoDataFrame - if gdf.crs is None: - print("CRS not found for GeoDataFrame. Please set it manually.") - return None - # Convert the combined geometry to a format that rioxarray can work with geometries = [mapping(roi)] From 42e4a7c0f85b18e18d8f1114e36d02a38027e924 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 9 Jan 2025 16:51:53 -0800 Subject: [PATCH 026/102] Bug Fix: correctly intialized some status flags to independently run step 14 --- src/rat/rat_basin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rat/rat_basin.py b/src/rat/rat_basin.py index eb51afce..ebc625ce 100644 --- a/src/rat/rat_basin.py +++ b/src/rat/rat_basin.py @@ -208,10 +208,10 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base ROUTING_STATUS = 1 GEE_STATUS = 1 ALTIMETER_STATUS = 1 - DELS_STATUS = 0 - EVAP_STATUS = 0 - OUTFLOW_STATUS = 0 - AEC_STATUS = 0 + DELS_STATUS = 1 + EVAP_STATUS = 1 + OUTFLOW_STATUS = 1 + AEC_STATUS = 1 except: no_errors = -1 rat_logger.exception("Error in Configuration parameters defined to run RAT.") From 32e6bc9b5afa547d8e63ce1037bc05887e214338 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 9 Jan 2025 16:52:28 -0800 Subject: [PATCH 027/102] Added ability to run aec files with comments --- src/rat/core/run_postprocessing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rat/core/run_postprocessing.py b/src/rat/core/run_postprocessing.py index 78ff0227..95a7c5b7 100644 --- a/src/rat/core/run_postprocessing.py +++ b/src/rat/core/run_postprocessing.py @@ -17,7 +17,7 @@ def calc_dels(aecpath, sareapath, savepath): - aec = pd.read_csv(aecpath) + aec = pd.read_csv(aecpath, comment='#') df = pd.read_csv(sareapath, parse_dates=['date']) df = df.drop_duplicates('date') From 60d85b680e3f01a0a32d1baa57c242a9b299408e Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 9 Jan 2025 16:56:31 -0800 Subject: [PATCH 028/102] Bug Fix: Corrected the calculation of cloud_percent for optical data (replaced nan before calculation) --- src/rat/core/sarea/TMS.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rat/core/sarea/TMS.py b/src/rat/core/sarea/TMS.py index 62035758..64aa8d1d 100644 --- a/src/rat/core/sarea/TMS.py +++ b/src/rat/core/sarea/TMS.py @@ -72,8 +72,8 @@ def tms_os(self, 'corrected_area_cordeiro': 'water_area_corrected' }, axis=1).set_index('date') l5df = l5df[['water_area_uncorrected', 'non_water_area', 'cloud_area', 'water_area_corrected']] - l5df['cloud_percent'] = l5df['cloud_area']*100/(l5df['water_area_uncorrected']+l5df['non_water_area']+l5df['cloud_area']) l5df.replace(-1, np.nan, inplace=True) + l5df['cloud_percent'] = l5df['cloud_area']*100/np.nansum(l5df['water_area_uncorrected'],l5df['non_water_area'],l5df['cloud_area']) # QUALITY_DESCRIPTION # 0: Good, not interpolated either due to missing data or high clouds @@ -111,8 +111,8 @@ def tms_os(self, 'corrected_area_cordeiro': 'water_area_corrected' }, axis=1).set_index('date') l7df = l7df[['water_area_uncorrected', 'non_water_area', 'cloud_area', 'water_area_corrected']] - l7df['cloud_percent'] = l7df['cloud_area']*100/(l7df['water_area_uncorrected']+l7df['non_water_area']+l7df['cloud_area']) l7df.replace(-1, np.nan, inplace=True) + l7df['cloud_percent'] = l7df['cloud_area']*100/np.nansum([l7df['water_area_uncorrected'],l7df['non_water_area'],l7df['cloud_area']]) # QUALITY_DESCRIPTION # 0: Good, not interpolated either due to missing data or high clouds @@ -150,8 +150,8 @@ def tms_os(self, 'corrected_area_cordeiro': 'water_area_corrected' }, axis=1).set_index('date') l8df = l8df[['water_area_uncorrected', 'non_water_area', 'cloud_area', 'water_area_corrected']] - l8df['cloud_percent'] = l8df['cloud_area']*100/(l8df['water_area_uncorrected']+l8df['non_water_area']+l8df['cloud_area']) l8df.replace(-1, np.nan, inplace=True) + l8df['cloud_percent'] = l8df['cloud_area']*100/np.nansum([l8df['water_area_uncorrected'],l8df['non_water_area'],l8df['cloud_area']]) # QUALITY_DESCRIPTION # 0: Good, not interpolated either due to missing data or high clouds @@ -190,8 +190,8 @@ def tms_os(self, 'corrected_area_cordeiro': 'water_area_corrected' }, axis=1).set_index('date') l9df = l9df[['water_area_uncorrected', 'non_water_area', 'cloud_area', 'water_area_corrected']] - l9df['cloud_percent'] = l9df['cloud_area']*100/(l9df['water_area_uncorrected']+l9df['non_water_area']+l9df['cloud_area']) l9df.replace(-1, np.nan, inplace=True) + l9df['cloud_percent'] = l9df['cloud_area']*100/np.nansum([l9df['water_area_uncorrected'],l9df['non_water_area'],l9df['cloud_area']]) # QUALITY_DESCRIPTION # 0: Good, not interpolated either due to missing data or high clouds From 7f7ffdb4fd1b948676c78b639b5944542998ee35 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 9 Jan 2025 16:57:22 -0800 Subject: [PATCH 029/102] Handled AEC creation for reservoirs built after DEM data retrieval --- src/rat/ee_utils/ee_aec_file_creator.py | 54 ++++++++++++++++--------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/src/rat/ee_utils/ee_aec_file_creator.py b/src/rat/ee_utils/ee_aec_file_creator.py index 435f3ca9..0959e03b 100644 --- a/src/rat/ee_utils/ee_aec_file_creator.py +++ b/src/rat/ee_utils/ee_aec_file_creator.py @@ -10,6 +10,7 @@ from scipy.integrate import cumulative_trapezoid from shapely.geometry import Point +WATER_SAREA_DIFF_Z_THRESHOLD = 3.0 BUFFER_DIST = 500 DEM = ee.Image('USGS/SRTMGL1_003') @@ -49,15 +50,25 @@ def get_obs_aec_above_water_surface(aec): Returns: pd.DataFrame: A DataFrame containing the filtered and processed AEC data with 'Elevation' and 'CumArea' columns. + Boolean: Boolean indicating whether the AEC data has water surface or not. """ obs_aec_above_water = aec.sort_values('Elevation') obs_aec_above_water['CumArea_diff'] = obs_aec_above_water['CumArea'].diff() - obs_aec_above_water['z_score'] = (obs_aec_above_water['CumArea_diff'] - obs_aec_above_water['CumArea'].mean()) / obs_aec_above_water['CumArea'].std() + obs_aec_above_water['z_score'] = (obs_aec_above_water['CumArea_diff'] - obs_aec_above_water['CumArea_diff'].mean()) / obs_aec_above_water['CumArea_diff'].std() + max_z_score = obs_aec_above_water['z_score'].max() max_z_core_idx = obs_aec_above_water['z_score'].idxmax() - obs_aec_above_water = obs_aec_above_water.loc[max_z_core_idx:, :] - obs_aec_above_water = obs_aec_above_water[['Elevation', 'CumArea']] - - return obs_aec_above_water + if max_z_score > WATER_SAREA_DIFF_Z_THRESHOLD: + obs_aec_above_water = obs_aec_above_water.loc[max_z_core_idx:, :] + obs_aec_above_water = obs_aec_above_water[['Elevation', 'CumArea']] + water_surface_exists = True + print(f"Clipped to elevations above water surface. Max Sarea difference zscore is {max_z_score}.") + else: + obs_aec_above_water = obs_aec_above_water[['Elevation', 'CumArea']] + water_surface_exists = False + print(f"Skipped clipping as No water surface was found using AEC because max 'sarea difference' zscore is {max_z_score}. Either the reservoir was created after DEM data was aquired or the AEC is already extrapolated.") + pass + + return obs_aec_above_water, water_surface_exists def calculate_storage(aec_df): """ @@ -269,7 +280,7 @@ def get_dam_bottom( # Load GRWL data grwl = gpd.read_file(grwl_fp) # Check if GRWL intersects with reservoir geometry - intersection = grwl.intersects(reservoir.union_all()) + intersection = grwl.intersects(reservoir.unary_union) else: intersection = np.array([False]) @@ -337,7 +348,13 @@ def extrapolate_reservoir( print(f"Dam bottom elevation for {reservoir_name} is {dam_bottom_elevation}") print(f"Dam top elevation for {reservoir_name} is {dam_top_elevation}") - aec = aec[(aec['Elevation'] > dam_bottom_elevation)&(aec['Elevation'] <= dam_top_elevation)] + aec_original = aec.copy() + # Remove elevations below and above dam's bottom and top elevation. If less than 5 observations are left, then just remove elevations below dam bottom. + aec = aec_original[(aec_original['Elevation'] > dam_bottom_elevation)&(aec_original['Elevation'] <= dam_top_elevation)] + if len(aec) > 5: + pass + else: + aec = aec_original[(aec_original['Elevation'] > dam_bottom_elevation)] x = aec['CumArea'] y = aec['Elevation'] @@ -402,6 +419,7 @@ def get_obs_aec_srtm(aec_dir_path, scale, reservoir, reservoir_name, clip_to_wat Returns: pd.DataFrame: A DataFrame containing the elevation and cumulative area data. + Boolean : Boolean indicating whether the AEC data has water surface or not. """ print(f"Generating observed AEC file for {reservoir_name} from SRTM") aec_dst_fp = os.path.join(aec_dir_path,reservoir_name+'.csv') @@ -413,8 +431,8 @@ def get_obs_aec_srtm(aec_dir_path, scale, reservoir, reservoir_name, clip_to_wat # done already. In that case, the 'CumArea' and 'Elevation_Observed' represent the SRTM # observed AEC. if 'Elevation_Observed' in aec_df.columns: - aec_df = aec_df[['CumArea', 'Elevation_Observed']].rename({'Elevation_Observed': 'Elevation'}, axis=1) - aec_df = aec_df.dropna(subset='Elevation') + # aec_df = aec_df[['CumArea', 'Elevation_Observed']].rename({'Elevation_Observed': 'Elevation'}, axis=1) + # aec_df = aec_df.dropna(subset='Elevation') clip_to_water_surf = False # in this case, clipping is not required. set it to false else: reservoir_polygon = reservoir.geometry @@ -458,14 +476,12 @@ def get_obs_aec_srtm(aec_dir_path, scale, reservoir, reservoir_name, clip_to_wat print(f"Observed AEC obtained from SRTM for {reservoir_name}") if clip_to_water_surf: - aec_df = get_obs_aec_above_water_surface( - aec_df - ) - print(f"Clipped to elevations above water surface") - + aec_df,water_surface_exists = get_obs_aec_above_water_surface(aec_df) + else: + water_surface_exists = False aec_df.to_csv(aec_dst_fp,index=False) - return aec_df + return aec_df, water_surface_exists def aec_file_creator( reservoir_shpfile, shpfile_column_dict, aec_dir_path, @@ -516,14 +532,16 @@ def aec_file_creator( dam_lon = float(reservoir[shpfile_column_dict['dam_lon']]) dam_location = Point(dam_lon, dam_lat) - aec = get_obs_aec_srtm(aec_dir_path, scale, reservoir, reservoir_name, clip_to_water_surf=True) + aec, water_surface_exists = get_obs_aec_srtm(aec_dir_path, scale, reservoir, reservoir_name, clip_to_water_surf=True) - if dam_height > 0: + if dam_height > 0 and water_surface_exists: extrapolate_reservoir( reservoir_gpd, dam_location, reservoir_name, dam_height, aec, aec_dir_path, grwl_fp=grwl_fp ) + elif not water_surface_exists: + print(f"No extrapolation was done in AEC for reservoir {reservoir_name} because of absence of water surface in AEC.") else: - print(f"Dam height can't be used to extrapolate aev: {dam_height}") + print(f"Dam height can't be used to extrapolate aev: {dam_height} for {reservoir_name}") return 1 From ff8cf46a184d5dc10d037a37fb8076caf0bd2704 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 9 Jan 2025 17:00:02 -0800 Subject: [PATCH 030/102] Bug Fix: Corrected the save file path of catchment climate ts --- src/rat/utils/convert_to_final_outputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rat/utils/convert_to_final_outputs.py b/src/rat/utils/convert_to_final_outputs.py index 84fca69e..23a53bb8 100644 --- a/src/rat/utils/convert_to_final_outputs.py +++ b/src/rat/utils/convert_to_final_outputs.py @@ -169,7 +169,7 @@ def convert_meteorological_ts(catchment_shpfile, catchments_gdf_column_dict, bas for index, row in catchments_spatial_filtered.iterrows(): res_name = row[catchments_gdf_column_dict['unique_identifier']] - save_file_path = dst_dir / res_name / '.csv' + save_file_path = dst_dir / (res_name+'.csv') catchment_roi = row['geometry'] print(f"Creating Catchment's Climatolgical time series for reservoir : {res_name}") create_meterological_ts(catchment_roi, meteorological_nc_file_path, save_file_path) From 770eb80511663cc82fd6d775bbe5172229bb0943 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 10 Jan 2025 11:16:55 -0800 Subject: [PATCH 031/102] Bug Fix: added square brackets for np.nansum --- src/rat/core/sarea/TMS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rat/core/sarea/TMS.py b/src/rat/core/sarea/TMS.py index 64aa8d1d..3abfd7d4 100644 --- a/src/rat/core/sarea/TMS.py +++ b/src/rat/core/sarea/TMS.py @@ -73,7 +73,7 @@ def tms_os(self, }, axis=1).set_index('date') l5df = l5df[['water_area_uncorrected', 'non_water_area', 'cloud_area', 'water_area_corrected']] l5df.replace(-1, np.nan, inplace=True) - l5df['cloud_percent'] = l5df['cloud_area']*100/np.nansum(l5df['water_area_uncorrected'],l5df['non_water_area'],l5df['cloud_area']) + l5df['cloud_percent'] = l5df['cloud_area']*100/np.nansum([l5df['water_area_uncorrected'],l5df['non_water_area'],l5df['cloud_area']]) # QUALITY_DESCRIPTION # 0: Good, not interpolated either due to missing data or high clouds From 6c4e51bcc6aaf0223a4fa96dc2b7b11eb4af9c7c Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 10 Jan 2025 11:23:15 -0800 Subject: [PATCH 032/102] S2_SR asset has been deprecated. Using S2_SR_HARMONIZED instead --- src/rat/core/sarea/sarea_cli_s2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index e35202e8..86a5701b 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -31,7 +31,7 @@ def grouper(iterable, n, *, incomplete='fill', fillvalue=None): raise ValueError('Expected fill, strict, or ignore') # NEW STUFF -s2 = ee.ImageCollection("COPERNICUS/S2_SR") +s2 = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED") gswd = ee.Image("JRC/GSW1_4/GlobalSurfaceWater") rgb_vis_params = {"bands":["B4","B3","B2"],"min":0,"max":0.4} From f0c89d643d4dda7aca586245ebad60ec22a87d54 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sat, 11 Jan 2025 00:44:14 -0800 Subject: [PATCH 033/102] Added the missing libraries in the dev environment --- environment.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/environment.yml b/environment.yml index 39729b6a..e95296f2 100644 --- a/environment.yml +++ b/environment.yml @@ -16,8 +16,14 @@ dependencies: - ruamel_yaml - yaml - gdown + - plotly - cfgrib - conda-build + - gfortran + - make + - gdal + - libnetcdf + - libgdal-netcdf - pip - pip: - geonetworkx \ No newline at end of file From 31c170c6401924cd04a2a5ae5df68b032aeb7731 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 12 Jan 2025 17:25:28 -0800 Subject: [PATCH 034/102] Added error handling for failing to create meteorological time series --- src/rat/rat_basin.py | 7 +- src/rat/toolbox/data_transform.py | 109 +++++++++++++++--------------- 2 files changed, 61 insertions(+), 55 deletions(-) diff --git a/src/rat/rat_basin.py b/src/rat/rat_basin.py index ebc625ce..2bb1aeea 100644 --- a/src/rat/rat_basin.py +++ b/src/rat/rat_basin.py @@ -809,8 +809,11 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base ## Climatological TS if (ROUTING_STATUS): - convert_meteorological_ts(catchment_vector_file_path, catchments_gdf_column_dict, basin_data, combined_datapath, final_output_path) - rat_logger.info("Converted Catchment's Climatological TS to the Output Format (from NetCDF).") + try: + convert_meteorological_ts(catchment_vector_file_path, catchments_gdf_column_dict, basin_data, combined_datapath, final_output_path) + rat_logger.info("Converted Catchment's Climatological TS to the Output Format (from NetCDF).") + except: + rat_logger.exception("Failed to convert Catchment's Climatological TS to the Output Format (from NetCDF).") else: rat_logger.info("Could not convert Catchment's Climatological TS to the Output Format (from NetCDF) as Routing run failed.") diff --git a/src/rat/toolbox/data_transform.py b/src/rat/toolbox/data_transform.py index eddc419a..7c98b4d7 100644 --- a/src/rat/toolbox/data_transform.py +++ b/src/rat/toolbox/data_transform.py @@ -64,57 +64,60 @@ def create_meterological_ts(roi, nc_file_path, output_csv_path): """ print("Creating meterological timeseries for a given geometry using comibined meteorlogical NetCDF produced by RAT.") - # Load the NetCDF file as an xarray Dataset - ds = xr.open_dataset(nc_file_path) - - # Ensure spatial dimensions are set correctly as x and y for rioxarray use - if 'lon' in ds.dims and 'lat' in ds.dims: - ds = ds.rename({'lon': 'x', 'lat': 'y'}) - elif 'longitude' in ds.dims and 'latitude' in ds.dims: - ds = ds.rename({'longitude': 'x', 'latitude': 'y'}) + if os.path.isfile(nc_file_path): + # Load the NetCDF file as an xarray Dataset + ds = xr.open_dataset(nc_file_path) + + # Ensure spatial dimensions are set correctly as x and y for rioxarray use + if 'lon' in ds.dims and 'lat' in ds.dims: + ds = ds.rename({'lon': 'x', 'lat': 'y'}) + elif 'longitude' in ds.dims and 'latitude' in ds.dims: + ds = ds.rename({'longitude': 'x', 'latitude': 'y'}) + else: + raise ValueError("Spatial dimensions not found. Expected 'lon', 'lat', 'longitude', or 'latitude'.") + + # Set spatial dimensions + ds = ds.rio.set_spatial_dims(x_dim='x', y_dim='y') + + # Sets default CRS for the dataset if missing + if ds.rio.crs is None: + print("CRS not found for dataset. Setting CRS to EPSG:4326.") + ds.rio.write_crs("EPSG:4326", inplace=True) + + # Convert the combined geometry to a format that rioxarray can work with + geometries = [mapping(roi)] + + # Clip the xarray Dataset using the ROI geometry + try: + ds_clipped = ds.rio.clip(geometries, from_disk=True) + except Exception as e: + print(f"Error during clipping: {e}") + return + + # Calculate the spatial average for each time step + spatial_mean = ds_clipped.mean(dim=['x', 'y']) + + # Convert the spatial mean to a pandas DataFrame + df = pd.DataFrame({ + 'time': spatial_mean.time.values, + 'precip': spatial_mean.precip.values, + 'tmin': spatial_mean.tmin.values, + 'tmax': spatial_mean.tmax.values, + 'wind': spatial_mean.wind.values + }) + + # Append the data if file exists + if os.path.exists(output_csv_path): + print(f"File {output_csv_path} exists. Appending new data and removing duplicates.") + existing_df = pd.read_csv(output_csv_path, parse_dates=['time']) + combined_df = pd.concat([existing_df, df], ignore_index=True) + # Remove duplicates, keeping the latest entry for each date + combined_df = combined_df.sort_values(by='time').drop_duplicates(subset='time', keep='last') + combined_df.to_csv(output_csv_path, index=False) + else: + # Save the new data + df.to_csv(output_csv_path, index=False) + + print(f"CSV file has been updated and saved to {output_csv_path}.") else: - raise ValueError("Spatial dimensions not found. Expected 'lon', 'lat', 'longitude', or 'latitude'.") - - # Set spatial dimensions - ds = ds.rio.set_spatial_dims(x_dim='x', y_dim='y') - - # Sets default CRS for the dataset if missing - if ds.rio.crs is None: - print("CRS not found for dataset. Setting CRS to EPSG:4326.") - ds.rio.write_crs("EPSG:4326", inplace=True) - - # Convert the combined geometry to a format that rioxarray can work with - geometries = [mapping(roi)] - - # Clip the xarray Dataset using the ROI geometry - try: - ds_clipped = ds.rio.clip(geometries, from_disk=True) - except Exception as e: - print(f"Error during clipping: {e}") - return - - # Calculate the spatial average for each time step - spatial_mean = ds_clipped.mean(dim=['x', 'y']) - - # Convert the spatial mean to a pandas DataFrame - df = pd.DataFrame({ - 'time': spatial_mean.time.values, - 'precip': spatial_mean.precip.values, - 'tmin': spatial_mean.tmin.values, - 'tmax': spatial_mean.tmax.values, - 'wind': spatial_mean.wind.values - }) - - # Append the data if file exists - if os.path.exists(output_csv_path): - print(f"File {output_csv_path} exists. Appending new data and removing duplicates.") - existing_df = pd.read_csv(output_csv_path, parse_dates=['time']) - combined_df = pd.concat([existing_df, df], ignore_index=True) - # Remove duplicates, keeping the latest entry for each date - combined_df = combined_df.sort_values(by='time').drop_duplicates(subset='time', keep='last') - combined_df.to_csv(output_csv_path, index=False) - else: - # Save the new data - df.to_csv(output_csv_path, index=False) - - print(f"CSV file has been updated and saved to {output_csv_path}.") \ No newline at end of file + raise ValueError(f"File {nc_file_path} does not exist.") \ No newline at end of file From 56de5cb8dd16b8daadce1904768d4e162938c099 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 12 Jan 2025 17:27:47 -0800 Subject: [PATCH 035/102] Corrected cloud percent by doing elementwise sum rather than column sum in denominator and added limit for interpolating l5 and l7 --- src/rat/core/sarea/TMS.py | 66 ++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/src/rat/core/sarea/TMS.py b/src/rat/core/sarea/TMS.py index 3abfd7d4..426f9e73 100644 --- a/src/rat/core/sarea/TMS.py +++ b/src/rat/core/sarea/TMS.py @@ -73,7 +73,7 @@ def tms_os(self, }, axis=1).set_index('date') l5df = l5df[['water_area_uncorrected', 'non_water_area', 'cloud_area', 'water_area_corrected']] l5df.replace(-1, np.nan, inplace=True) - l5df['cloud_percent'] = l5df['cloud_area']*100/np.nansum([l5df['water_area_uncorrected'],l5df['non_water_area'],l5df['cloud_area']]) + l5df['cloud_percent'] = l5df['cloud_area'] * 100 / l5df[['water_area_uncorrected', 'non_water_area', 'cloud_area']].sum(axis=1, skipna=True) # QUALITY_DESCRIPTION # 0: Good, not interpolated either due to missing data or high clouds @@ -96,8 +96,8 @@ def tms_os(self, l5df_interpolated.loc[np.isnan(l5df_interpolated['non_water_area']), 'non_water_area'] = 0 l5df_interpolated.loc[np.isnan(l5df_interpolated['water_area_uncorrected']), 'water_area_uncorrected'] = 0 - # Interpolate bad data - l5df_interpolated.loc[:, "water_area_corrected"] = l5df_interpolated.loc[:, "water_area_corrected"].interpolate(method="linear", limit_direction="forward") + # Interpolate bad data (max upto 6-7 months (seasonal) for l5) + l5df_interpolated.loc[:, "water_area_corrected"] = l5df_interpolated.loc[:, "water_area_corrected"].interpolate(method="linear", limit_direction="forward", limit=13) l5df_interpolated['sat'] = 'l5' TO_MERGE.append(l5df_interpolated) @@ -112,7 +112,7 @@ def tms_os(self, }, axis=1).set_index('date') l7df = l7df[['water_area_uncorrected', 'non_water_area', 'cloud_area', 'water_area_corrected']] l7df.replace(-1, np.nan, inplace=True) - l7df['cloud_percent'] = l7df['cloud_area']*100/np.nansum([l7df['water_area_uncorrected'],l7df['non_water_area'],l7df['cloud_area']]) + l7df['cloud_percent'] = l7df['cloud_area'] * 100 / l7df[['water_area_uncorrected', 'non_water_area', 'cloud_area']].sum(axis=1, skipna=True) # QUALITY_DESCRIPTION # 0: Good, not interpolated either due to missing data or high clouds @@ -135,8 +135,8 @@ def tms_os(self, l7df_interpolated.loc[np.isnan(l7df_interpolated['non_water_area']), 'non_water_area'] = 0 l7df_interpolated.loc[np.isnan(l7df_interpolated['water_area_uncorrected']), 'water_area_uncorrected'] = 0 - # Interpolate bad data - l7df_interpolated.loc[:, "water_area_corrected"] = l7df_interpolated.loc[:, "water_area_corrected"].interpolate(method="linear", limit_direction="forward") + # Interpolate bad data (max upto 6-7 months (seasonal) for l7) + l7df_interpolated.loc[:, "water_area_corrected"] = l7df_interpolated.loc[:, "water_area_corrected"].interpolate(method="linear", limit_direction="forward", limit=13) l7df_interpolated['sat'] = 'l7' TO_MERGE.append(l7df_interpolated) @@ -151,7 +151,7 @@ def tms_os(self, }, axis=1).set_index('date') l8df = l8df[['water_area_uncorrected', 'non_water_area', 'cloud_area', 'water_area_corrected']] l8df.replace(-1, np.nan, inplace=True) - l8df['cloud_percent'] = l8df['cloud_area']*100/np.nansum([l8df['water_area_uncorrected'],l8df['non_water_area'],l8df['cloud_area']]) + l8df['cloud_percent'] = l8df['cloud_area'] * 100 / l8df[['water_area_uncorrected', 'non_water_area', 'cloud_area']].sum(axis=1, skipna=True) # QUALITY_DESCRIPTION # 0: Good, not interpolated either due to missing data or high clouds @@ -191,7 +191,7 @@ def tms_os(self, }, axis=1).set_index('date') l9df = l9df[['water_area_uncorrected', 'non_water_area', 'cloud_area', 'water_area_corrected']] l9df.replace(-1, np.nan, inplace=True) - l9df['cloud_percent'] = l9df['cloud_area']*100/np.nansum([l9df['water_area_uncorrected'],l9df['non_water_area'],l9df['cloud_area']]) + l9df['cloud_percent'] = l9df['cloud_area'] * 100 / l9df[['water_area_uncorrected', 'non_water_area', 'cloud_area']].sum(axis=1, skipna=True) # QUALITY_DESCRIPTION # 0: Good, not interpolated either due to missing data or high clouds @@ -315,7 +315,24 @@ def tms_os(self, optical_with_no_sar.loc[:, 'days_passed'] = optical.index.to_series().diff().dt.days.fillna(0) # Calculate smoothed values with moving weighted average method if more than 7 values; weights are calculated using cloud percent. if len(optical_with_no_sar)>7: - optical_with_no_sar['filled_area'] = weighted_moving_average(optical_with_no_sar['non-smoothened optical area'], weights = (101-optical_with_no_sar['cloud_percent']),window_size=3) + # Temporarily interpolate missing values + optical_with_no_sar['interpolated_area'] = optical_with_no_sar['non-smoothened optical area'].interpolate(method='linear') + + # Apply weighted moving average using interpolated values + optical_with_no_sar['smoothed_area'] = weighted_moving_average( + optical_with_no_sar['interpolated_area'], + weights=(101 - optical_with_no_sar['cloud_percent']), + window_size=3 + ) + + # Restore NaN values to preserve the original structure + optical_with_no_sar['filled_area'] = optical_with_no_sar['smoothed_area'].where( + ~optical_with_no_sar['non-smoothened optical area'].isna() + ) + + # Drop temporary columns + optical_with_no_sar = optical_with_no_sar.drop(['interpolated_area', 'smoothed_area'], axis=1) + # Drop 'area' column from optical_with_no_sar optical_with_no_sar = optical_with_no_sar.drop('area',axis=1) # Optical with SAR @@ -324,7 +341,23 @@ def tms_os(self, result = pd.concat([optical_with_no_sar,optical_with_sar],axis=0) # Smoothen the combined surface area estimates to avoid noise or peaks using savgol_filter if more than 9 values (to increase smoothness and include more points as we have both TMS-OS and Optical) if len(result)>9: - result['filled_area'] = savgol_filter(result['filled_area'], window_length=7, polyorder=3) + # Temporarily interpolate missing values for smoothing + result['interpolated_area'] = result['filled_area'].interpolate(method='linear') + + # Apply Savitzky-Golay filter + result['smoothed_filled_area'] = savgol_filter( + result['interpolated_area'], window_length=7, polyorder=3 + ) + + # Restore NaN values + filled_area_with_nans = result['smoothed_filled_area'].where( + ~result['filled_area'].isna() + ) + result['filled_area'] = filled_area_with_nans + + # Drop temporary columns + result = result.drop(['interpolated_area', 'smoothed_filled_area'], axis=1) + method = 'Combine' # If SAR begins before Optical else: @@ -336,8 +369,17 @@ def tms_os(self, result.loc[:, 'days_passed'] = optical.index.to_series().diff().dt.days.fillna(0) # Calculate smoothed values with Savitzky-Golay method if more than 7 values if len(result)>7: - result['filled_area'] = weighted_moving_average(result['non-smoothened optical area'], weights = (101-result['cloud_percent']),window_size=3) - result['filled_area'] = savgol_filter(result['filled_area'], window_length=7, polyorder=3) + filled_area_temp = weighted_moving_average( + result['non-smoothened optical area'].interpolate(method='linear'), # Temporary interpolate for calculation + weights=(101 - result['cloud_percent']), + window_size=3 + ) + + # Apply Savitzky-Golay filter + smoothed_area_temp = savgol_filter(filled_area_temp, window_length=7, polyorder=3) + + # Reapply NaN mask to maintain sparse points + result['filled_area'] = pd.Series(smoothed_area_temp).where(~result['non-smoothened optical area'].isna()) method = 'Optical' # Returning method used for surface area estimation return result,method From 33ada0e7f8fe3489cda3bc0bdc3cfefca235eb85 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 12 Jan 2025 17:28:26 -0800 Subject: [PATCH 036/102] Added mission end date for landsat 5 to finish processing early --- src/rat/core/sarea/sarea_cli_l5.py | 10 +++++++--- src/rat/core/sarea/sarea_cli_l7.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_l5.py b/src/rat/core/sarea/sarea_cli_l5.py index 84c7204f..c3fd376c 100644 --- a/src/rat/core/sarea/sarea_cli_l5.py +++ b/src/rat/core/sarea/sarea_cli_l5.py @@ -1,5 +1,5 @@ import ee -from datetime import datetime, timedelta, timezone +from datetime import date, datetime, timedelta, timezone import pandas as pd import time import os @@ -32,7 +32,7 @@ NIR_BAND_NAME = 'SR_B4' SWIR1_BAND_NAME = 'SR_B5' SWIR2_BAND_NAME = 'SR_B7' - +MISSION_END_DATE = date(2012,5,6) # last date of landsat 5 data on GEE def preprocess(im): clipped = im # clipping adds processing overhead, setting clipped = im @@ -414,7 +414,7 @@ def run_process_long(res_name, res_polygon, start, end, datadir): # Read the existing file temp_df = pd.read_csv(savepath, parse_dates=['mosaic_enddate']).set_index('mosaic_enddate') - # Get the last date in the existing file and adjust the first observation to before last date (last date might not be for this satellite. Its TMS-OS data's ;ast date.) + # Get the last date in the existing file and adjust the first observation to before last date (last date might not be for this satellite. Its TMS-OS data's last date.) last_date = temp_df.index[-1].to_pydatetime() fo = (last_date - timedelta(days=TEMPORAL_RESOLUTION*2)).strftime("%Y-%m-%d") # Create an array with filepath @@ -447,6 +447,10 @@ def run_process_long(res_name, res_polygon, start, end, datadir): for subset_dates in grouped_dates: try: print(subset_dates) + # Check if the start of subset_dates is after the end date of the mission. If so quit. + if subset_dates[0].date() > MISSION_END_DATE: + print(f"Reached mission end date. No further data available from Landsat-5 satellite mission in GEE - {MISSION_END_DATE}") + break # Convert dates list to earth engine object dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) # Generate Timeseries of one image corresponding to each date with water area in its attributes diff --git a/src/rat/core/sarea/sarea_cli_l7.py b/src/rat/core/sarea/sarea_cli_l7.py index 8c30ee70..5b90e3dd 100644 --- a/src/rat/core/sarea/sarea_cli_l7.py +++ b/src/rat/core/sarea/sarea_cli_l7.py @@ -1,5 +1,5 @@ import ee -from datetime import datetime, timedelta, timezone +from datetime import date, datetime, timedelta, timezone import pandas as pd import time import os From a551c593a01204f6391a50a4f6ca2debadedcc97 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 12 Jan 2025 17:29:20 -0800 Subject: [PATCH 037/102] Evaporation can now be calculated even if vic results are deleted but not for the first time. In future it can be made independent easily. --- src/rat/core/run_postprocessing.py | 120 +++++++++++++++++------------ 1 file changed, 70 insertions(+), 50 deletions(-) diff --git a/src/rat/core/run_postprocessing.py b/src/rat/core/run_postprocessing.py index 95a7c5b7..71056e9c 100644 --- a/src/rat/core/run_postprocessing.py +++ b/src/rat/core/run_postprocessing.py @@ -50,28 +50,70 @@ def calc_dels(aecpath, sareapath, savepath): df.to_csv(savepath, index=False) def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, savepath, forecast_mode=False): - ds = xr.open_dataset(vic_res_path) - forcings_ds = xr.open_mfdataset(forcings_path, engine='netcdf4') - ## Slicing the latest run time period - ds = ds.sel(time=slice(start_date, end_date)) - forcings_ds = forcings_ds.sel(time=slice(start_date, end_date)) - - # create buffer to get required bounds - res_geom = res_data.geometry - res_buf = res_geom.buffer(0.1) - - minx, miny, maxx, maxy = res_buf.bounds - - log.debug(f"Bounds: {res_buf.bounds}") - log.debug("Clipping forcings") - forcings_ds_clipped = forcings_ds['air_pressure'].sel(lon=slice(minx, maxx), lat=slice(maxy, miny)) - forcings = forcings_ds_clipped.load() - - log.debug("Clipping VIC results") - ds_clipped = ds.sel(lon=slice(minx, maxx), lat=slice(maxy, miny)) - reqvars_clipped = ds_clipped[['OUT_EVAP', 'OUT_R_NET', 'OUT_VP', 'OUT_WIND', 'OUT_AIR_TEMP']] - reqvars = reqvars_clipped.load() + # Initialize existing_data to None + existing_data = None + # If vic result file exists, then use that to calculate evaporation + if os.path.isfile(vic_res_path): + ds = xr.open_dataset(vic_res_path) + forcings_ds = xr.open_mfdataset(forcings_path, engine='netcdf4') + ## Slicing the latest run time period + ds = ds.sel(time=slice(start_date, end_date)) + forcings_ds = forcings_ds.sel(time=slice(start_date, end_date)) + + # create buffer to get required bounds + res_geom = res_data.geometry + res_buf = res_geom.buffer(0.1) + + minx, miny, maxx, maxy = res_buf.bounds + + log.debug(f"Bounds: {res_buf.bounds}") + log.debug("Clipping forcings") + forcings_ds_clipped = forcings_ds['air_pressure'].sel(lon=slice(minx, maxx), lat=slice(maxy, miny)) + forcings = forcings_ds_clipped.load() + + log.debug("Clipping VIC results") + ds_clipped = ds.sel(lon=slice(minx, maxx), lat=slice(maxy, miny)) + reqvars_clipped = ds_clipped[['OUT_EVAP', 'OUT_R_NET', 'OUT_VP', 'OUT_WIND', 'OUT_AIR_TEMP']] + reqvars = reqvars_clipped.load() + + log.debug("Checking if grid cells lie inside reservoir") + last_layer = reqvars.isel(time=-1).to_dataframe().reset_index() + temp_gdf = gpd.GeoDataFrame(last_layer, geometry=gpd.points_from_xy(last_layer.lon, last_layer.lat)) + points_within = temp_gdf[temp_gdf.within(res_geom)]['geometry'] + + if len(points_within) == 0: + log.debug("No points inside reservoir, using nearest point to centroid") + centroid = res_geom.centroid + + data = reqvars.sel(lat=centroid.y, lon=centroid.x, method='nearest') + data = data.to_dataframe().reset_index()[1:].set_index('time') + P = forcings.sel(lat=centroid.y, lon=centroid.x, method='nearest') + P = P.to_dataframe().reset_index().set_index('time')['air_pressure'].resample('1D').mean()[1:] + P.head() + + else: + # print(f"[!] {len(points_within)} Grid cells inside reservoir found, averaging their values") + data = reqvars.sel(lat=np.array(points_within.y), lon=np.array(points_within.x), method='nearest').to_dataframe().reset_index().groupby('time').mean()[1:] + + P = forcings.sel(lat=np.array(points_within.y), lon=np.array(points_within.x), method='nearest').resample({'time':'1D'}).mean().to_dataframe().groupby('time').mean()[1:] + + data['P'] = P + # If no vic result file exists then check if data is there in existing file between start and end time + elif os.path.isfile(savepath): + existing_data = pd.read_csv(savepath, parse_dates=['time']) + # Check if it has data before end_date + if pd.Timestamp(end_date) in existing_data['time'].values: + # Filter data between start_date and end_date (inclusive) + data = existing_data[ + (existing_data['time'] >= pd.Timestamp(start_date)) & + (existing_data['time'] <= pd.Timestamp(end_date)) + ] + else: + raise ValueError("VIC result file not found. Existing evaporation rat_outputs file also does not have data for the requested dates.") + else: + raise ValueError("VIC result file or any existing evaporation rat_outputs file not found.") + # get sarea - if string, read from file, else use same surface area value for all time steps log.debug(f"Getting surface areas - {sarea}") if isinstance(sarea, str): @@ -86,53 +128,31 @@ def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, s ix = pd.date_range(start=first_obs, end=end_date, freq='D') sarea_interpolated = sarea_interpolated.reindex(ix).fillna(method='ffill') sarea_interpolated = sarea_interpolated[start_date:end_date] - - log.debug("Checking if grid cells lie inside reservoir") - last_layer = reqvars.isel(time=-1).to_dataframe().reset_index() - temp_gdf = gpd.GeoDataFrame(last_layer, geometry=gpd.points_from_xy(last_layer.lon, last_layer.lat)) - points_within = temp_gdf[temp_gdf.within(res_geom)]['geometry'] - - if len(points_within) == 0: - log.debug("No points inside reservoir, using nearest point to centroid") - centroid = res_geom.centroid - - data = reqvars.sel(lat=centroid.y, lon=centroid.x, method='nearest') - data = data.to_dataframe().reset_index()[1:].set_index('time') - - P = forcings.sel(lat=centroid.y, lon=centroid.x, method='nearest') - P = P.to_dataframe().reset_index().set_index('time')['air_pressure'].resample('1D').mean()[1:] - P.head() - - else: - # print(f"[!] {len(points_within)} Grid cells inside reservoir found, averaging their values") - data = reqvars.sel(lat=np.array(points_within.y), lon=np.array(points_within.x), method='nearest').to_dataframe().reset_index().groupby('time').mean()[1:] - - P = forcings.sel(lat=np.array(points_within.y), lon=np.array(points_within.x), method='nearest').resample({'time':'1D'}).mean().to_dataframe().groupby('time').mean()[1:] - + if isinstance(sarea, pd.DataFrame): data['area'] = sarea_interpolated['area'] else: data['area'] = sarea - data['P'] = P + data = data.dropna() if (data.empty): print('After removal of NAN values, no data left to calculate evaporation.') return None else: data['penman_E'] = data.apply(lambda row: penman(row['OUT_R_NET'], row['OUT_AIR_TEMP'], row['OUT_WIND'], row['OUT_VP'], row['P'], row['area']), axis=1) + data =data.rename({'penman_E': 'OUT_EVAP'}, axis=1) data = data.reset_index() # Save (Writing new file if not exist otherwise append) - if os.path.isfile(savepath): - existing_data = pd.read_csv(savepath, parse_dates=['time']) - new_data = data[['time', 'penman_E']].rename({'penman_E': 'OUT_EVAP'}, axis=1) + if existing_data: + new_data = data.copy() # Concat the two dataframes into a new dataframe holding all the data (memory intensive): complement = pd.concat([existing_data, new_data], ignore_index=True) # Remove all duplicates: complement.drop_duplicates(subset=['time'],inplace=True, keep='last') complement.to_csv(savepath, index=False) else: - data[['time', 'penman_E']].rename({'penman_E': 'OUT_EVAP'}, axis=1).to_csv(savepath, index=False) + data.to_csv(savepath, index=False) def calc_outflow(inflowpath, dspath, epath, area, savepath): @@ -268,7 +288,7 @@ def run_postprocessing(basin_name, basin_data_dir, reservoir_shpfile, reservoir_ log.debug(f"Calculating Evaporation for {reservoir_name}") calc_E(reservoir, start_date_str_evap, end_date_str, forcings_path, vic_results_path, sarea, e_path, forecast_mode=forecast_mode) else: - raise Exception("Surface area file not found; skipping evaporation calculation") + raise Exception("Surface area or VIC result file not found; skipping evaporation calculation") except: log.exception(f"Evaporation for {reservoir_name} could not be calculated.") no_failed_files +=1 From 768808e4aeaf9897fbb7c63e18dbf4ce5313573b Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 12 Jan 2025 17:58:07 -0800 Subject: [PATCH 038/102] Added routing workspace, landsat 5,7 & 9 scratch folders to cleaning script --- src/rat/utils/clean.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/rat/utils/clean.py b/src/rat/utils/clean.py index 659a7598..44bf2483 100644 --- a/src/rat/utils/clean.py +++ b/src/rat/utils/clean.py @@ -78,6 +78,12 @@ def clean_routing(self): except: print("No rout_outputs folder to delete") + try: + rout_workspace_path = os.path.join(self.basin_data_dir,'ro','wkspc','') + shutil.rmtree(rout_workspace_path) + except: + print("No routing wkspc folder to delete") + try: rout_init_states_dir_path = os.path.join(self.basin_data_dir,'ro','rout_state_file','') days_old = self.days_old_to_delete #n max of days @@ -96,12 +102,30 @@ def clean_routing(self): print("No rout init_state file to delete") def clean_gee(self): + try: + l5_scratch_path = os.path.join(self.basin_data_dir,'gee','gee_sarea_tmsos','l5','_scratch') + shutil.rmtree(l5_scratch_path) + except: + print("No _scratch folder to delete for landsat-5 based reserevoir area extraction") + + try: + l7_scratch_path = os.path.join(self.basin_data_dir,'gee','gee_sarea_tmsos','l7','_scratch') + shutil.rmtree(l7_scratch_path) + except: + print("No _scratch folder to delete for landsat-7 based reserevoir area extraction") + try: l8_scratch_path = os.path.join(self.basin_data_dir,'gee','gee_sarea_tmsos','l8','_scratch') shutil.rmtree(l8_scratch_path) except: print("No _scratch folder to delete for landsat-8 based reserevoir area extraction") + try: + l9_scratch_path = os.path.join(self.basin_data_dir,'gee','gee_sarea_tmsos','l9','_scratch') + shutil.rmtree(l9_scratch_path) + except: + print("No _scratch folder to delete for landsat-9 based reserevoir area extraction") + try: s2_scratch_path = os.path.join(self.basin_data_dir,'gee','gee_sarea_tmsos','s2','_scratch') shutil.rmtree(s2_scratch_path) From 0488fde8f87c1fead9f646e387857078bff5cf25 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 12 Jan 2025 19:09:48 -0800 Subject: [PATCH 039/102] Bug Fix: corrected AEC extrapolation code to handle missing Dam Height --- src/rat/ee_utils/ee_aec_file_creator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rat/ee_utils/ee_aec_file_creator.py b/src/rat/ee_utils/ee_aec_file_creator.py index 0959e03b..4cfbe2fb 100644 --- a/src/rat/ee_utils/ee_aec_file_creator.py +++ b/src/rat/ee_utils/ee_aec_file_creator.py @@ -527,7 +527,10 @@ def aec_file_creator( reservoir_gpd = reservoir_gpd.set_crs(reservoirs_polygon.crs) reservoir_name = str(reservoir[shpfile_column_dict['unique_identifier']]) - dam_height = float(reservoir[shpfile_column_dict['dam_height']]) + if reservoir[shpfile_column_dict['dam_height']] is not None: + dam_height = float(reservoir[shpfile_column_dict['dam_height']]) + else: + dam_height = -99 dam_lat = float(reservoir[shpfile_column_dict['dam_lat']]) dam_lon = float(reservoir[shpfile_column_dict['dam_lon']]) dam_location = Point(dam_lon, dam_lat) From 1cc6cf60144334c3a8cb2dc716c8493f9eb6f30b Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 13 Jan 2025 03:29:52 -0800 Subject: [PATCH 040/102] Added warning about the number of reservoirs for which creating meteorological time series fails in level 1 log. --- src/rat/rat_basin.py | 7 +++++-- src/rat/utils/convert_to_final_outputs.py | 20 +++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/rat/rat_basin.py b/src/rat/rat_basin.py index 2bb1aeea..81474b33 100644 --- a/src/rat/rat_basin.py +++ b/src/rat/rat_basin.py @@ -810,8 +810,11 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base ## Climatological TS if (ROUTING_STATUS): try: - convert_meteorological_ts(catchment_vector_file_path, catchments_gdf_column_dict, basin_data, combined_datapath, final_output_path) - rat_logger.info("Converted Catchment's Climatological TS to the Output Format (from NetCDF).") + no_failed_reservoirs = convert_meteorological_ts(catchment_vector_file_path, catchments_gdf_column_dict, basin_data, combined_datapath, final_output_path) + if no_failed_reservoirs: + rat_logger.warning(f"Could not extract Catchment's Climatological TS for {no_failed_reservoirs} reservoir(s).") + else: + rat_logger.info("Converted Catchment's Climatological TS to the Output Format (from NetCDF).") except: rat_logger.exception("Failed to convert Catchment's Climatological TS to the Output Format (from NetCDF).") else: diff --git a/src/rat/utils/convert_to_final_outputs.py b/src/rat/utils/convert_to_final_outputs.py index 23a53bb8..634e10b8 100644 --- a/src/rat/utils/convert_to_final_outputs.py +++ b/src/rat/utils/convert_to_final_outputs.py @@ -167,14 +167,20 @@ def convert_meteorological_ts(catchment_shpfile, catchments_gdf_column_dict, bas else: catchments_spatial_filtered = catchments.copy() + failed_res_no = 0 for index, row in catchments_spatial_filtered.iterrows(): - res_name = row[catchments_gdf_column_dict['unique_identifier']] - save_file_path = dst_dir / (res_name+'.csv') - catchment_roi = row['geometry'] - print(f"Creating Catchment's Climatolgical time series for reservoir : {res_name}") - create_meterological_ts(catchment_roi, meteorological_nc_file_path, save_file_path) - - return + try: + res_name = row[catchments_gdf_column_dict['unique_identifier']] + save_file_path = dst_dir / (res_name+'.csv') + catchment_roi = row['geometry'] + print(f"Creating Catchment's Climatolgical time series for reservoir : {res_name}") + create_meterological_ts(catchment_roi, meteorological_nc_file_path, save_file_path) + except Exception as e: + failed_res_no = failed_res_no + 1 + print(f"Error during creating meteorlogical TS for {res_name}: {e}") + continue + + return failed_res_no else: return From 974a7a8128d3e3cd95c5cdf9461b3b91860e8446 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 13 Jan 2025 03:31:27 -0800 Subject: [PATCH 041/102] Added evaporation calculation from previous file if no vic results or forcings available. In future can be segregated to first create file without Evap column and this step can then use that file to calculate evaporation. --- src/rat/core/run_postprocessing.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/rat/core/run_postprocessing.py b/src/rat/core/run_postprocessing.py index 71056e9c..55a9a3ab 100644 --- a/src/rat/core/run_postprocessing.py +++ b/src/rat/core/run_postprocessing.py @@ -53,7 +53,7 @@ def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, s # Initialize existing_data to None existing_data = None # If vic result file exists, then use that to calculate evaporation - if os.path.isfile(vic_res_path): + if os.path.isfile(vic_res_path) and os.path.isfile(forcings_path): ds = xr.open_dataset(vic_res_path) forcings_ds = xr.open_mfdataset(forcings_path, engine='netcdf4') ## Slicing the latest run time period @@ -73,7 +73,7 @@ def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, s log.debug("Clipping VIC results") ds_clipped = ds.sel(lon=slice(minx, maxx), lat=slice(maxy, miny)) - reqvars_clipped = ds_clipped[['OUT_EVAP', 'OUT_R_NET', 'OUT_VP', 'OUT_WIND', 'OUT_AIR_TEMP']] + reqvars_clipped = ds_clipped[['OUT_R_NET', 'OUT_VP', 'OUT_WIND', 'OUT_AIR_TEMP']] reqvars = reqvars_clipped.load() log.debug("Checking if grid cells lie inside reservoir") @@ -102,15 +102,14 @@ def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, s # If no vic result file exists then check if data is there in existing file between start and end time elif os.path.isfile(savepath): existing_data = pd.read_csv(savepath, parse_dates=['time']) - # Check if it has data before end_date - if pd.Timestamp(end_date) in existing_data['time'].values: - # Filter data between start_date and end_date (inclusive) - data = existing_data[ - (existing_data['time'] >= pd.Timestamp(start_date)) & - (existing_data['time'] <= pd.Timestamp(end_date)) - ] - else: - raise ValueError("VIC result file not found. Existing evaporation rat_outputs file also does not have data for the requested dates.") + existing_data = existing_data.loc[:, ~existing_data.columns.str.startswith('OUT_EVAP')] + # Filter data between start_date and end_date (inclusive) + data = existing_data[ + (existing_data['time'] >= pd.Timestamp(start_date)) & + (existing_data['time'] <= pd.Timestamp(end_date)) + ] + data = data.set_index('time').sort_values(by='time') + else: raise ValueError("VIC result file or any existing evaporation rat_outputs file not found.") @@ -127,7 +126,8 @@ def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, s if forecast_mode: # forecast mode. extrapolate using forward fill. ix = pd.date_range(start=first_obs, end=end_date, freq='D') sarea_interpolated = sarea_interpolated.reindex(ix).fillna(method='ffill') - sarea_interpolated = sarea_interpolated[start_date:end_date] + # Slicing is exclusive of end date. But we want inclusive of both start and end dates. + sarea_interpolated = sarea_interpolated.loc[sarea_interpolated.index.isin(data.index)] if isinstance(sarea, pd.DataFrame): data['area'] = sarea_interpolated['area'] @@ -140,11 +140,12 @@ def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, s return None else: data['penman_E'] = data.apply(lambda row: penman(row['OUT_R_NET'], row['OUT_AIR_TEMP'], row['OUT_WIND'], row['OUT_VP'], row['P'], row['area']), axis=1) + data =data.rename({'penman_E': 'OUT_EVAP'}, axis=1) data = data.reset_index() # Save (Writing new file if not exist otherwise append) - if existing_data: + if existing_data is not None: new_data = data.copy() # Concat the two dataframes into a new dataframe holding all the data (memory intensive): complement = pd.concat([existing_data, new_data], ignore_index=True) From 2a643c2076b6423b3e4b12aac7437280650b05b3 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 13 Jan 2025 03:31:57 -0800 Subject: [PATCH 042/102] Corrected warning for skipping inflow calculation if routing output is not available. --- src/rat/core/run_routing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rat/core/run_routing.py b/src/rat/core/run_routing.py index a86af263..8890d88a 100644 --- a/src/rat/core/run_routing.py +++ b/src/rat/core/run_routing.py @@ -136,7 +136,8 @@ def generate_inflow(src_dir, dst_dir): files = [src_dir / f for f in src_dir.glob('*.day')] if len(files)==0: - raise Exception("No routing output was found to generate inflow.") + log_level1.warning("No routing output was found to generate inflow. Skipping inflow calculation.") + return if not dst_dir.exists(): log.error("Directory does not exist: %s", dst_dir) From c92474eb53d33fd8988e892f1fd25b4fc60aff09 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 13 Jan 2025 03:33:04 -0800 Subject: [PATCH 043/102] Bug Fix: If Dam Height is NONE, print it in level2 log --- src/rat/ee_utils/ee_aec_file_creator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rat/ee_utils/ee_aec_file_creator.py b/src/rat/ee_utils/ee_aec_file_creator.py index 4cfbe2fb..e754c020 100644 --- a/src/rat/ee_utils/ee_aec_file_creator.py +++ b/src/rat/ee_utils/ee_aec_file_creator.py @@ -545,6 +545,9 @@ def aec_file_creator( elif not water_surface_exists: print(f"No extrapolation was done in AEC for reservoir {reservoir_name} because of absence of water surface in AEC.") else: - print(f"Dam height can't be used to extrapolate aev: {dam_height} for {reservoir_name}") + if reservoir[shpfile_column_dict['dam_height']] is None: + print(f"Dam height is not available to extrapolate AEC for {reservoir_name}.") + else: + print(f"Dam height can't be used to extrapolate AEC: {dam_height} for {reservoir_name}") return 1 From 9d4ab52deafc0016e5c96a0f3acc0b93d572bac2 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 13 Jan 2025 03:33:45 -0800 Subject: [PATCH 044/102] Bug Fix: Handled clipping of very small catchment ROI compared to resolution of combined data --- src/rat/toolbox/data_transform.py | 45 ++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/rat/toolbox/data_transform.py b/src/rat/toolbox/data_transform.py index 7c98b4d7..b778e59b 100644 --- a/src/rat/toolbox/data_transform.py +++ b/src/rat/toolbox/data_transform.py @@ -3,7 +3,7 @@ import xarray as xr import rioxarray as rxr import pandas as pd -from shapely.geometry import mapping +from shapely.geometry import mapping, Polygon, MultiPolygon def create_meterological_ts(roi, nc_file_path, output_csv_path): """ @@ -78,22 +78,49 @@ def create_meterological_ts(roi, nc_file_path, output_csv_path): # Set spatial dimensions ds = ds.rio.set_spatial_dims(x_dim='x', y_dim='y') + + # Get the dataset resolution in degrees + res_x, res_y = ds.rio.resolution() # Dataset's resolution in x and y directions + + # Calculate half of the resolution size to add a buffer if needed + half_pixel_size = max(abs(res_x), abs(res_y)) / 2 + + # Check if the ROI needs to be expanded with a buffer + if isinstance(roi, (Polygon, MultiPolygon)): + roi_bounds = roi.bounds # (minx, miny, maxx, maxy) + roi_width = roi_bounds[2] - roi_bounds[0] + roi_height = roi_bounds[3] - roi_bounds[1] + + # Apply buffer conditionally: apply buffer if ROI is smaller than the resolution + if roi_width < abs(res_x) or roi_height < abs(res_y): + print("Catchment ROI is too small compared to resolution, applying buffer.") + roi_expanded = roi.buffer(half_pixel_size) # Buffer using half of the dataset's resolution + else: + print("Catchment ROI is sufficiently large, no buffer applied.") + roi_expanded = roi # No buffer needed, keep original ROI + else: + raise ValueError("Catchment ROI must be a polygon or a mulipolygon.") # Sets default CRS for the dataset if missing if ds.rio.crs is None: print("CRS not found for dataset. Setting CRS to EPSG:4326.") ds.rio.write_crs("EPSG:4326", inplace=True) - # Convert the combined geometry to a format that rioxarray can work with - geometries = [mapping(roi)] - - # Clip the xarray Dataset using the ROI geometry try: - ds_clipped = ds.rio.clip(geometries, from_disk=True) - except Exception as e: - print(f"Error during clipping: {e}") - return + # Convert the combined geometry to a format that rioxarray can work with + geometries = [mapping(roi_expanded)] + # Clip the xarray Dataset using the ROI geometry + ds_clipped = ds.rio.clip(geometries, from_disk=True) + except: + print("Catchment ROI is still not large enough. Clipping with all touching pixels for original catchment ROI.") + # Convert the combined geometry to a format that rioxarray can work with + geometries = [mapping(roi)] + + # Clip the xarray Dataset using the ROI geometry + ds_clipped = ds.rio.clip(geometries, all_touched = True, from_disk=True) + + # Calculate the spatial average for each time step spatial_mean = ds_clipped.mean(dim=['x', 'y']) From 85ec9bebe35cf58f503389c1ea042576308bd3dd Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 13 Jan 2025 04:03:32 -0800 Subject: [PATCH 045/102] Removed temporary correction to fix an earlier bug --- src/rat/core/run_postprocessing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rat/core/run_postprocessing.py b/src/rat/core/run_postprocessing.py index 55a9a3ab..e204cfa4 100644 --- a/src/rat/core/run_postprocessing.py +++ b/src/rat/core/run_postprocessing.py @@ -102,7 +102,6 @@ def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, s # If no vic result file exists then check if data is there in existing file between start and end time elif os.path.isfile(savepath): existing_data = pd.read_csv(savepath, parse_dates=['time']) - existing_data = existing_data.loc[:, ~existing_data.columns.str.startswith('OUT_EVAP')] # Filter data between start_date and end_date (inclusive) data = existing_data[ (existing_data['time'] >= pd.Timestamp(start_date)) & From 3b2052acf886576c9737737b1d92804ec4db86d0 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Wed, 15 Jan 2025 02:35:43 -0800 Subject: [PATCH 046/102] Meaning as mandatory step Z (Final step) and independent of step -14; Removed default value of steps if left empty in config file --- src/rat/rat_basin.py | 47 +++++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/src/rat/rat_basin.py b/src/rat/rat_basin.py index 81474b33..fe25ffb2 100644 --- a/src/rat/rat_basin.py +++ b/src/rat/rat_basin.py @@ -50,6 +50,7 @@ # Step-12: Generating Area Elevation Curves for reservoirs # Step-13: Calculation of Inflow, Outflow, Evaporation and Storage change # Step-14: Conversion of output data to final format as time series +# Step-Z: Cleaning up of memory space by removal of unwanted data #TODO: Converting steps to separate modules to make RAT more robust and generalized #module-1 step-1,2 data_preparation_vic @@ -96,7 +97,8 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base if config['GLOBAL'].get('steps'): steps = config['GLOBAL']['steps'] else: - steps = [1,2,3,4,5,6,7,8,9,10,11,12,13,14] + steps = [] + rat_logger.warning("RAT is being executed without any defined steps to process. Please use steps parameter in GLOBAL section of configuration file.") # Defining resolution to run RAT xres = 0.0625 @@ -198,7 +200,7 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base # Clearing out previous rat outputs so that the new data does not gets appended. if(config['CLEAN_UP']['clean_previous_outputs']): - rat_logger.info("Clearing up memory space: Removal of previous rat outputs, routing inflow, extracted altimetry data and gee extracted surface area time series") + rat_logger.info("Clearing up memory space: Removal of previous rat outputs, routing outputs (streamflow), extracted altimetry data, and gee extracted surface area and NSSC time series") cleaner.clean_previous_outputs() # Initializing Status for different models & tasks (1 for successful run & 0 for failed run) @@ -213,7 +215,7 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base OUTFLOW_STATUS = 1 AEC_STATUS = 1 except: - no_errors = -1 + no_errors = no_errors + 1 rat_logger.exception("Error in Configuration parameters defined to run RAT.") return (no_errors, latest_altimetry_cycle) else: @@ -382,7 +384,7 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base ## End of defining paths for storing post-processed data and webformat data except: - no_errors = -1 + no_errors = no_errors+1 rat_logger.exception("Error in creating required directory structure for RAT") return (no_errors, latest_altimetry_cycle) else: @@ -791,7 +793,7 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base try: rat_logger.info("Starting Step-14: Creating final outputs in a timeseries format and cleaning up.") - ##---------- Convert all time-series to final output csv format and clean up----------## + ##---------- Convert all time-series to final output csv format --------------------## ## Surface Area if(GEE_STATUS): convert_sarea(sarea_savepath,final_output_path) @@ -879,8 +881,23 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base runResorr(basin_data_dir,basin_station_latlon_file,resorr_startDate,resorr_endDate) else: rat_logger.warning("No station latlon file found to run RESORR. Try running Step-8 or provide station_latlon_path in routing section of config file.") - + + except: + no_errors = no_errors+1 + rat_logger.exception("Error Executing Step-14: Creating final outputs in a timeseries format and cleaning up.") + else: + rat_logger.info("Finished Step-14: Creating final outputs in a timeseries format and cleaning up.") + ##----------Converted all time-series to final output csv format -----------------## + + ######### Step-Z Mandatory Last Step + if (config['GLOBAL'].get('cleaning')): + try: + ##------------------ Cleaning up the memory --------------------## + rat_logger.info("Starting to clean up memory space by removing unwanted data.") # Clearing out memory space as per user input + if(config['CLEAN_UP'].get('clean_processing')): + rat_logger.info("Clearing up memory space: Removal of preprocessed meteorolgical data (not combined NetCDF) and post-processed AEC files.") + cleaner.clean_processing() if(config['CLEAN_UP'].get('clean_metsim')): rat_logger.info("Clearing up memory space: Removal of metsim input and output files") cleaner.clean_metsim() @@ -888,7 +905,7 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base rat_logger.info("Clearing up memory space: Removal of vic input, output files and previous init_state_files older than 20 days.") cleaner.clean_vic() if(config['CLEAN_UP'].get('clean_routing')): - rat_logger.info("Clearing up memory space: Removal of routing input, output files and previous rout_state_files older than 20 days.") + rat_logger.info("Clearing up memory space: Removal of routing input files and previous rout_state_files older than 20 days.") cleaner.clean_routing() if(config['CLEAN_UP'].get('clean_gee')): rat_logger.info("Clearing up memory space: Removal of unwanted gee extracted small chunk files") @@ -896,12 +913,20 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base if(config['CLEAN_UP'].get('clean_altimetry')): rat_logger.info("Clearing up memory space: Removal of raw altimetry downloaded data files.") cleaner.clean_altimetry() + if(config['CLEAN_UP'].get('clean_basin_parameter_files')): + rat_logger.info("Clearing up memory space: Removal of basin's parameter files (for VIC, routing, gee and other) created by RAT.") + cleaner.clean_basin_parameter_files() + if(config['CLEAN_UP'].get('clean_basin_meteorological_data')): + rat_logger.info("Clearing up memory space: Removal of basin's meteorological data in the combined NetCDf format.") + cleaner.clean_basin_meteorological_data() + except: no_errors = no_errors+1 - rat_logger.exception("Error Executing Step-14: Creating final outputs in a timeseries format and cleaning up.") + rat_logger.exception("Error in cleaning up memory space by removal of unwanted data.") else: - rat_logger.info("Finished Step-14: Creating final outputs in a timeseries format and cleaning up.") - ##----------Converted all time-series to final output csv format and cleaned up----------## - + rat_logger.info("Finished cleaning up memory space and removed unwanted data.") + ##------------------ Cleaned up the memory --------------------## + + close_logger() return (no_errors, latest_altimetry_cycle) \ No newline at end of file From 73c8b68ad080ee4067893f8793ef96843b97d982 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Wed, 15 Jan 2025 02:36:39 -0800 Subject: [PATCH 047/102] Added options to clean up basin parameter files and combined NetCDF meteorological file --- src/rat/utils/clean.py | 66 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/src/rat/utils/clean.py b/src/rat/utils/clean.py index 44bf2483..ae1c7637 100644 --- a/src/rat/utils/clean.py +++ b/src/rat/utils/clean.py @@ -9,18 +9,18 @@ def __init__(self, basin_data_dir): self.days_old_to_delete = 20 pass - def clean_pre_processing(self): + def clean_processing(self): try: pre_processing_processed_path = os.path.join(self.basin_data_dir,'pre_processing','processed','') shutil.rmtree(pre_processing_processed_path) except: print("No processed folder in pre_processing to delete") - # try: - # pre_processing_nc_path = os.path.join(self.basin_data_dir,'pre_processing','nc','') - # shutil.rmtree(pre_processing_nc_path) - # except: - # print("No nc folder in pre_processing to delete") + try: + post_processing_path = os.path.join(self.basin_data_dir,'post_processing','') + shutil.rmtree(post_processing_path) + except: + print("No nc folder in pre_processing to delete") def clean_metsim(self): try: @@ -71,12 +71,6 @@ def clean_routing(self): shutil.rmtree(rout_inputs_path) except: print("No rout_inputs folder to delete") - - try: - rout_outputs_path = os.path.join(self.basin_data_dir,'ro','ou','') - shutil.rmtree(rout_outputs_path) - except: - print("No rout_outputs folder to delete") try: rout_workspace_path = os.path.join(self.basin_data_dir,'ro','wkspc','') @@ -145,6 +139,12 @@ def clean_previous_outputs(self): shutil.rmtree(rat_outputs_path) except: print("No previous rat_outputs folder to delete") + + try: + rout_outputs_path = os.path.join(self.basin_data_dir,'ro','ou','') + shutil.rmtree(rout_outputs_path) + except: + print("No rout_outputs folder to delete") try: gee_sarea_path = os.path.join(self.basin_data_dir,'gee','gee_sarea_tmsos') @@ -152,6 +152,12 @@ def clean_previous_outputs(self): except: print("No previous gee extracted surface area data folder to delete") + try: + gee_nssc_path = os.path.join(self.basin_data_dir,'gee','gee_nssc') + shutil.rmtree(gee_nssc_path) + except: + print("No previous gee extracted NSSC data folder to delete") + try: altimetry_timeseries_path = os.path.join(self.basin_data_dir,'altimetry','altimetry_timeseries') shutil.rmtree(altimetry_timeseries_path) @@ -169,3 +175,39 @@ def clean_previous_outputs(self): shutil.rmtree(final_outputs_path) except: print("No final_outputs folder to delete with previous outputs") + + def clean_basin_parameter_files(self): + try: + basin_grid_data_path = os.path.join(self.basin_data_dir,'basin_grid_data') + shutil.rmtree(basin_grid_data_path) + except: + print("No basin_grid_data folder to delete") + + try: + vic_basin_params_path = os.path.join(self.basin_data_dir,'vic','vic_basin_params') + shutil.rmtree(vic_basin_params_path) + except: + print("No vic_basin_params folder to delete inside vic") + + try: + ro_basin_params_path = os.path.join(self.basin_data_dir,'ro','pars') + shutil.rmtree(ro_basin_params_path) + except: + print("No pars folder to delete inside ro") + + try: + gee_basin_params_path = os.path.join(self.basin_data_dir,'gee','gee_basin_params') + shutil.rmtree(gee_basin_params_path) + except: + print("No gee_basin_params folder to delete inside gee") + + def clean_basin_meteorological_data(self): + try: + basin_meteorological_combined_path = os.path.join(self.basin_data_dir,'pre_processing','nc') + shutil.rmtree(basin_meteorological_combined_path) + except: + print("No nc folder (containing basin's combined meteorological file) to delete inside pre_processing") + + + + From bc74c54c7f95ba097a4fe110071844984e930286 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Wed, 15 Jan 2025 02:41:21 -0800 Subject: [PATCH 048/102] Bug FIx: forcings path is not a single path --- src/rat/core/run_postprocessing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rat/core/run_postprocessing.py b/src/rat/core/run_postprocessing.py index e204cfa4..ae73ecd6 100644 --- a/src/rat/core/run_postprocessing.py +++ b/src/rat/core/run_postprocessing.py @@ -1,4 +1,5 @@ import os +import glob import pandas as pd import numpy as np import xarray as xr @@ -53,7 +54,7 @@ def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, s # Initialize existing_data to None existing_data = None # If vic result file exists, then use that to calculate evaporation - if os.path.isfile(vic_res_path) and os.path.isfile(forcings_path): + if os.path.isfile(vic_res_path) and glob.glob(forcings_path): ds = xr.open_dataset(vic_res_path) forcings_ds = xr.open_mfdataset(forcings_path, engine='netcdf4') ## Slicing the latest run time period From 43226ebc326f45f8e9db64e2a4eb31ee23182469 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Tue, 21 Jan 2025 14:47:37 -0800 Subject: [PATCH 049/102] made multiple_basins parameter optional --- src/rat/run_rat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rat/run_rat.py b/src/rat/run_rat.py index 856e87ad..15db7f31 100644 --- a/src/rat/run_rat.py +++ b/src/rat/run_rat.py @@ -75,7 +75,7 @@ def run_rat(config_fn, operational_latency=None ): sys.stderr = sys.__stderr__ ############################ ----------- Single basin run ---------------- ###################################### - if(not config['GLOBAL']['multiple_basin_run']): + if(not config['GLOBAL'].get('multiple_basin_run')): log.info('############## Starting RAT for '+config['BASIN']['basin_name']+' #################') # Checking if Rat is running operationally with some latency. If yes, update start, end and vic_init_state dates. From 2d99224cacc1461a4069c23961ee6e76e95a927e Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 24 Jan 2025 10:39:24 -0800 Subject: [PATCH 050/102] corrected spelling of 'successfully' --- src/rat/run_rat.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rat/run_rat.py b/src/rat/run_rat.py index 15db7f31..386892bb 100644 --- a/src/rat/run_rat.py +++ b/src/rat/run_rat.py @@ -67,7 +67,7 @@ def run_rat(config_fn, operational_latency=None ): with StringIO() as fake_stderr, redirect_stderr(fake_stderr): ee_credentials = ee.ServiceAccountCredentials(ee_configuration.service_account,ee_configuration.key_file) ee.Initialize(ee_credentials) - log.info("Connected to earth engine succesfully.") + log.info("Connected to earth engine successfully.") except Exception as e: log.error(f"Failed to connect to Earth Engine. RAT will not be able to use Surface Area Estimations. Error: {e}") finally: @@ -147,7 +147,7 @@ def run_rat(config_fn, operational_latency=None ): if(forecast_no_errors>0): log.info('############## RAT-Forecasting run finished for '+config_copy['BASIN']['basin_name']+ ' with '+str(forecast_no_errors)+' error(s). #################') elif(forecast_no_errors==0): - log.info('############## Succesfully run RAT-Forecasting for '+config_copy['BASIN']['basin_name']+' #################') + log.info('############## Successfully run RAT-Forecasting for '+config_copy['BASIN']['basin_name']+' #################') else: log.error('############## RAT-Forecasting run failed for '+config_copy['BASIN']['basin_name']+' #################') # Displaying and storing RAT function outputs in the copy (non-mutabled as it was not passes to function) @@ -157,7 +157,7 @@ def run_rat(config_fn, operational_latency=None ): if(no_errors>0): log.info('############## RAT run finished for '+config['BASIN']['basin_name']+ ' with '+str(no_errors)+' error(s). #################') elif(no_errors==0): - log.info('############## Succesfully run RAT for '+config['BASIN']['basin_name']+' #################') + log.info('############## Successfully run RAT for '+config['BASIN']['basin_name']+' #################') else: log.error('############## RAT run failed for '+config['BASIN']['basin_name']+' #################') @@ -306,7 +306,7 @@ def run_rat(config_fn, operational_latency=None ): if(forecast_no_errors>0): log.info('############## RAT-Forecasting run finished for '+config_copy['BASIN']['basin_name']+ ' with '+str(forecast_no_errors)+' error(s). #################') elif(forecast_no_errors==0): - log.info('############## Succesfully run RAT-Forecasting for '+config_copy['BASIN']['basin_name']+' #################') + log.info('############## Successfully run RAT-Forecasting for '+config_copy['BASIN']['basin_name']+' #################') else: log.error('############## RAT-Forecasting run failed for '+config_copy['BASIN']['basin_name']+' #################') # Displaying and storing RAT function outputs @@ -319,7 +319,7 @@ def run_rat(config_fn, operational_latency=None ): if(no_errors>0): log.info('############## RAT run finished for '+config_copy['BASIN']['basin_name']+ ' with '+str(no_errors)+' error(s). #################') elif(no_errors==0): - log.info('############## Succesfully run RAT for '+config_copy['BASIN']['basin_name']+' #################') + log.info('############## Successfully run RAT for '+config_copy['BASIN']['basin_name']+' #################') else: log.error('############## RAT run failed for '+config_copy['BASIN']['basin_name']+' #################') From 5774777ac032bb1c048041fdf6b7bb3a4d00632b Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 24 Jan 2025 16:46:09 -0800 Subject: [PATCH 051/102] AEC extrapolation even when dam height is not available --- src/rat/ee_utils/ee_aec_file_creator.py | 37 +++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/rat/ee_utils/ee_aec_file_creator.py b/src/rat/ee_utils/ee_aec_file_creator.py index e754c020..36d129fd 100644 --- a/src/rat/ee_utils/ee_aec_file_creator.py +++ b/src/rat/ee_utils/ee_aec_file_creator.py @@ -342,19 +342,27 @@ def extrapolate_reservoir( Returns: pd.DataFrame: DataFrame containing the predicted storage values with columns 'CumArea', 'Elevation', 'Storage', 'Storage (mil. m3)', and 'Elevation_Observed'. """ + aec_original = aec.copy() + dam_bottom_elevation, method = get_dam_bottom(reservoir, buffer_distance=buffer_distance, dam_location=dam_location, grwl_fp=grwl_fp) # from GRWL downstream point - dam_top_elevation = dam_bottom_elevation + dam_height - print(f"Dam bottom elevation for {reservoir_name} is {dam_bottom_elevation}") - print(f"Dam top elevation for {reservoir_name} is {dam_top_elevation}") - - aec_original = aec.copy() - # Remove elevations below and above dam's bottom and top elevation. If less than 5 observations are left, then just remove elevations below dam bottom. - aec = aec_original[(aec_original['Elevation'] > dam_bottom_elevation)&(aec_original['Elevation'] <= dam_top_elevation)] - if len(aec) > 5: - pass + + if dam_height>0: + dam_top_elevation = dam_bottom_elevation + dam_height + print(f"Dam height for {reservoir_name} is {dam_height}") + print(f"Dam top elevation for {reservoir_name} is {dam_top_elevation}") + + # Remove elevations below and above dam's bottom and top elevation. If less than 5 observations are left, then just remove elevations below dam bottom. + aec = aec_original[(aec_original['Elevation'] > dam_bottom_elevation)&(aec_original['Elevation'] <= dam_top_elevation)] + if len(aec) > 5: + pass + else: + aec = aec_original[(aec_original['Elevation'] > dam_bottom_elevation)] else: + print(f"Dam height is not available for {reservoir_name} : {dam_height}") + print("Using all the elevation data above dam bottom elevation.") aec = aec_original[(aec_original['Elevation'] > dam_bottom_elevation)] + x = aec['CumArea'] y = aec['Elevation'] @@ -530,24 +538,19 @@ def aec_file_creator( if reservoir[shpfile_column_dict['dam_height']] is not None: dam_height = float(reservoir[shpfile_column_dict['dam_height']]) else: - dam_height = -99 + dam_height = np.nan dam_lat = float(reservoir[shpfile_column_dict['dam_lat']]) dam_lon = float(reservoir[shpfile_column_dict['dam_lon']]) dam_location = Point(dam_lon, dam_lat) aec, water_surface_exists = get_obs_aec_srtm(aec_dir_path, scale, reservoir, reservoir_name, clip_to_water_surf=True) - if dam_height > 0 and water_surface_exists: + if water_surface_exists: extrapolate_reservoir( reservoir_gpd, dam_location, reservoir_name, dam_height, aec, aec_dir_path, grwl_fp=grwl_fp ) - elif not water_surface_exists: - print(f"No extrapolation was done in AEC for reservoir {reservoir_name} because of absence of water surface in AEC.") else: - if reservoir[shpfile_column_dict['dam_height']] is None: - print(f"Dam height is not available to extrapolate AEC for {reservoir_name}.") - else: - print(f"Dam height can't be used to extrapolate AEC: {dam_height} for {reservoir_name}") + print(f"No extrapolation was done in AEC for reservoir {reservoir_name} because of absence of water surface in AEC.") return 1 From 7066094566102680165fa0f041f0906e9464c2bc Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sat, 25 Jan 2025 15:16:11 -0800 Subject: [PATCH 052/102] BUG Fix: Round Up function round down negative numbers --- src/rat/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rat/utils/utils.py b/src/rat/utils/utils.py index d787ce08..ecf5df01 100644 --- a/src/rat/utils/utils.py +++ b/src/rat/utils/utils.py @@ -30,7 +30,7 @@ def round_up(n, decimals=0): if n>0: return math.ceil(n * multiplier) / multiplier else: - return -math.ceil(abs(n) * multiplier) / multiplier + return -math.floor(abs(n) * multiplier) / multiplier # https://gist.github.com/pritamd47/e7ddc49f25ae7f1b06c201f0a8b98348 # Clip time-series From 7d91e0f5225cf93d377a21566122f85a3ef612e0 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sat, 25 Jan 2025 15:30:45 -0800 Subject: [PATCH 053/102] improvement: Step 9 now always create basin_reservoir_shapefile with latest user given shapefile --- src/rat/rat_basin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/rat/rat_basin.py b/src/rat/rat_basin.py index fe25ffb2..3ab6ac10 100644 --- a/src/rat/rat_basin.py +++ b/src/rat/rat_basin.py @@ -691,9 +691,8 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base rat_logger.info("Starting Step-9: Preparation of parameter files for Surface Area Calculation") #------------- Selection of Reservoirs within the basin begins--------------# ###### Preparing basin's reservoir shapefile and it's associated column dictionary for calculating surface area ##### - ### Creating Basin Reservoir Shapefile, if not exists ### - if not os.path.exists(basin_reservoir_shpfile_path): - create_basin_reservoir_shpfile(config['GEE']['reservoir_vector_file'], reservoirs_gdf_column_dict, basin_data, + ### Creating Basin Reservoir Shapefile, overwritten if exists ### + create_basin_reservoir_shpfile(config['GEE']['reservoir_vector_file'], reservoirs_gdf_column_dict, basin_data, config['ROUTING']['station_global_data'], basin_reservoir_shpfile_path) ###### Prepared basin's reservoir shapefile and it's associated column dictionary ##### except: From 4d92c11b6ad5f28a0c25d81d2b1ba0ebaa160849 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 30 Jan 2025 03:23:25 -0800 Subject: [PATCH 054/102] Error handling for dates when error is too many concurrent aggregations for landsat 5,6,,8 and 9 --- src/rat/core/sarea/sarea_cli_l5.py | 145 ++++++++++++++---------- src/rat/core/sarea/sarea_cli_l7.py | 140 +++++++++++++---------- src/rat/core/sarea/sarea_cli_l8.py | 173 +++++++++++++++------------- src/rat/core/sarea/sarea_cli_l9.py | 174 ++++++++++++++++------------- 4 files changed, 357 insertions(+), 275 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_l5.py b/src/rat/core/sarea/sarea_cli_l5.py index c3fd376c..24b0c369 100644 --- a/src/rat/core/sarea/sarea_cli_l5.py +++ b/src/rat/core/sarea/sarea_cli_l5.py @@ -25,6 +25,7 @@ CLOUD_COVER_LIMIT = 90 TEMPORAL_RESOLUTION = 16 RESULTS_PER_ITER = 5 +MIN_RESULTS_PER_ITER = 1 QUALITY_PIXEL_BAND_NAME = 'QA_PIXEL' BLUE_BAND_NAME = 'SR_B1' GREEN_BAND_NAME = 'SR_B2' @@ -386,7 +387,7 @@ def get_first_obs(start_date, end_date): str_fmt = 'YYYY-MM-dd' return ee.Date.parse(str_fmt, ee.Date(first_im.get('system:time_start')).format(str_fmt)) -def run_process_long(res_name, res_polygon, start, end, datadir): +def run_process_long(res_name, res_polygon, start, end, datadir, results_per_iter=RESULTS_PER_ITER): fo = start #fo: first observation enddate = end @@ -441,68 +442,94 @@ def run_process_long(res_name, res_polygon, start, end, datadir): # Creating list of dates from fo to enddate with frequency of TEMPORAL_RESOLUTION dates = pd.date_range(fo, enddate, freq=f'{TEMPORAL_RESOLUTION}D') # Grouping dates into smaller arrays to process in GEE - grouped_dates = grouper(dates, RESULTS_PER_ITER) + grouped_dates = grouper(dates, results_per_iter) - # For each smaller array of dates - for subset_dates in grouped_dates: + # Until results per iteration is less than min results per iteration + while results_per_iter >= MIN_RESULTS_PER_ITER: + # try to run for each subset of dates try: - print(subset_dates) - # Check if the start of subset_dates is after the end date of the mission. If so quit. - if subset_dates[0].date() > MISSION_END_DATE: - print(f"Reached mission end date. No further data available from Landsat-5 satellite mission in GEE - {MISSION_END_DATE}") - break - # Convert dates list to earth engine object - dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) - # Generate Timeseries of one image corresponding to each date with water area in its attributes - res = generate_timeseries(dates).filterMetadata('l5_images', 'greater_than', 0) - # Extracting uncorrected water area and other information from attributes - uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l5_images', - 'water_red_sum', 'water_green_sum', 'water_nir_sum','water_red_green_mean','water_nir_red_mean'] - uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') - uncorrected_final_data = uncorrected_final_data_ee.getInfo() - print("Uncorrected", uncorrected_final_data) - # Extracting corrected area after corrrecting for cloud covered pixels using zhao gao correction. - res_corrected_cordeiro = res.map(lambda im: postprocess_wrapper(im, 'water_map_cordeiro', im.get('water_area_cordeiro'))) - corrected_columns_to_extract = ['to_date', 'corrected_area'] - corrected_final_data_cordeiro_ee = res_corrected_cordeiro \ - .filterMetadata('corrected_area', 'not_equals', None) \ - .reduceColumns( - ee.Reducer.toList( - len(corrected_columns_to_extract)), - corrected_columns_to_extract - ).get('list') - corrected_final_data_cordeiro = corrected_final_data_cordeiro_ee.getInfo() - print("Corrected - Cordeiro", corrected_final_data_cordeiro) - # If no data point for this duration, then skip - if len(uncorrected_final_data) == 0: - continue - # Create pandas dataframes with the extracted information and merge them - uncorrected_df = pd.DataFrame(uncorrected_final_data, columns=uncorrected_columns_to_extract) - corrected_cordeiro_df = pd.DataFrame(corrected_final_data_cordeiro, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_cordeiro'}, axis=1) - df = pd.merge(uncorrected_df, corrected_cordeiro_df, 'left', 'to_date') - - df['from_date'] = pd.to_datetime(df['from_date'], format="%Y-%m-%d") - df['to_date'] = pd.to_datetime(df['to_date'], format="%Y-%m-%d") - df['mosaic_enddate'] = df['to_date'] - pd.Timedelta(1, unit='day') - df = df.set_index('mosaic_enddate') - print(df.head(2)) - # Save the dataframe on the disk - fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") - df.to_csv(fname) - # Create a randonm sleep time - s_time = randint(20, 30) - print(f"Sleeping for {s_time} seconds") - time.sleep(randint(20, 30)) - - if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: - print(f"Quitting: Reached enddate {enddate}") - break - elif df.index[-1].strftime('%Y-%m-%d') == fo: - print(f"Reached last available observation - {fo}") - break + # For each smaller array of dates + for subset_dates in grouped_dates: + try: + print(subset_dates) + # Check if the start of subset_dates is after the end date of the mission. If so quit. + if subset_dates[0].date() > MISSION_END_DATE: + print(f"Reached mission end date. No further data available from Landsat-5 satellite mission in GEE - {MISSION_END_DATE}") + break + # Convert dates list to earth engine object + dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) + # Generate Timeseries of one image corresponding to each date with water area in its attributes + res = generate_timeseries(dates).filterMetadata('l5_images', 'greater_than', 0) + # Extracting uncorrected water area and other information from attributes + uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l5_images', + 'water_red_sum', 'water_green_sum', 'water_nir_sum','water_red_green_mean','water_nir_red_mean'] + uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') + uncorrected_final_data = uncorrected_final_data_ee.getInfo() + print("Uncorrected", uncorrected_final_data) + # Extracting corrected area after corrrecting for cloud covered pixels using zhao gao correction. + res_corrected_cordeiro = res.map(lambda im: postprocess_wrapper(im, 'water_map_cordeiro', im.get('water_area_cordeiro'))) + corrected_columns_to_extract = ['to_date', 'corrected_area'] + corrected_final_data_cordeiro_ee = res_corrected_cordeiro \ + .filterMetadata('corrected_area', 'not_equals', None) \ + .reduceColumns( + ee.Reducer.toList( + len(corrected_columns_to_extract)), + corrected_columns_to_extract + ).get('list') + corrected_final_data_cordeiro = corrected_final_data_cordeiro_ee.getInfo() + print("Corrected - Cordeiro", corrected_final_data_cordeiro) + # If no data point for this duration, then skip + if len(uncorrected_final_data) == 0: + continue + # Create pandas dataframes with the extracted information and merge them + uncorrected_df = pd.DataFrame(uncorrected_final_data, columns=uncorrected_columns_to_extract) + corrected_cordeiro_df = pd.DataFrame(corrected_final_data_cordeiro, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_cordeiro'}, axis=1) + df = pd.merge(uncorrected_df, corrected_cordeiro_df, 'left', 'to_date') + + df['from_date'] = pd.to_datetime(df['from_date'], format="%Y-%m-%d") + df['to_date'] = pd.to_datetime(df['to_date'], format="%Y-%m-%d") + df['mosaic_enddate'] = df['to_date'] - pd.Timedelta(1, unit='day') + df = df.set_index('mosaic_enddate') + print(df.head(2)) + # Save the dataframe on the disk + fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") + df.to_csv(fname) + # Create a randonm sleep time + s_time = randint(20, 30) + print(f"Sleeping for {s_time} seconds") + time.sleep(randint(20, 30)) + + if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: + print(f"Quitting: Reached enddate {enddate}") + break + elif df.index[-1].strftime('%Y-%m-%d') == fo: + print(f"Reached last available observation - {fo}") + break + except Exception as e: + log.error(e) + # Adjust results_per_iter only if error includes "Too many concurrent aggregations" + if "Too many concurrent aggregations" in str(e): + results_per_iter -= 1 + print(f"Reducing Results per iteration to {results_per_iter} due to error.") + if results_per_iter < MIN_RESULTS_PER_ITER: + print("Minimum Results per iteration reached. Continuing to next group of dates.") + results_per_iter = MIN_RESULTS_PER_ITER + continue + else: + raise Exception(f'Reducing Results per iteration to {results_per_iter}.') + else: + continue + # This exception will be only raised if the error is "Too many concurrent aggregations". + # and Results per iteration will be reduced but still be greater than or equal to minimum results per iteration. + # We will continue while loop and for loop within while loop from the left over grouped dates. except Exception as e: - log.error(e) + dates = pd.date_range(subset_dates[0], enddate, freq=f'{TEMPORAL_RESOLUTION}D') + grouped_dates = grouper(dates, results_per_iter) continue + # In case no exception is raised and the complete for loop ran succesfully, break the while loop + # because we need to run the for loop only once. + else: + break # Combine the files into one database to_combine.extend([os.path.join(savedir, f) for f in os.listdir(savedir) if f.endswith(".csv")]) diff --git a/src/rat/core/sarea/sarea_cli_l7.py b/src/rat/core/sarea/sarea_cli_l7.py index 5b90e3dd..0800b6c6 100644 --- a/src/rat/core/sarea/sarea_cli_l7.py +++ b/src/rat/core/sarea/sarea_cli_l7.py @@ -25,6 +25,7 @@ CLOUD_COVER_LIMIT = 90 TEMPORAL_RESOLUTION = 16 RESULTS_PER_ITER = 5 +MIN_RESULTS_PER_ITER = 1 QUALITY_PIXEL_BAND_NAME = 'QA_PIXEL' BLUE_BAND_NAME = 'SR_B1' GREEN_BAND_NAME = 'SR_B2' @@ -420,7 +421,7 @@ def get_first_obs(start_date, end_date): str_fmt = 'YYYY-MM-dd' return ee.Date.parse(str_fmt, ee.Date(first_im.get('system:time_start')).format(str_fmt)) -def run_process_long(res_name, res_polygon, start, end, datadir): +def run_process_long(res_name, res_polygon, start, end, datadir, results_per_iter=RESULTS_PER_ITER): fo = start #fo: first observation enddate = end @@ -475,66 +476,91 @@ def run_process_long(res_name, res_polygon, start, end, datadir): # Creating list of dates from fo to enddate with frequency of TEMPORAL_RESOLUTION dates = pd.date_range(fo, enddate, freq=f'{TEMPORAL_RESOLUTION}D') # Grouping dates into smaller arrays to process in GEE - grouped_dates = grouper(dates, RESULTS_PER_ITER) + grouped_dates = grouper(dates, results_per_iter) - # For each smaller array of dates - for subset_dates in grouped_dates: + # Until results per iteration is less than min results per iteration + while results_per_iter >= MIN_RESULTS_PER_ITER: + # try to run for each subset of dates try: - print(subset_dates) - # Convert dates list to earth engine object - dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) - # Generate Timeseries of one image corresponding to each date with water area in its attributes - res = generate_timeseries(dates).filterMetadata('l7_images', 'greater_than', 0) - # Extracting uncorrected water area and other information from attributes - uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l7_images', - 'water_red_sum', 'water_green_sum', 'water_nir_sum','water_red_green_mean','water_nir_red_mean'] - uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') - uncorrected_final_data = uncorrected_final_data_ee.getInfo() - print("Uncorrected", uncorrected_final_data) - # Extracting corrected area after corrrecting for cloud covered pixels using zhao gao correction. - res_corrected_cordeiro = res.map(lambda im: postprocess_wrapper(im, 'water_map_cordeiro', im.get('water_area_cordeiro'))) - corrected_columns_to_extract = ['to_date', 'corrected_area'] - corrected_final_data_cordeiro_ee = res_corrected_cordeiro \ - .filterMetadata('corrected_area', 'not_equals', None) \ - .reduceColumns( - ee.Reducer.toList( - len(corrected_columns_to_extract)), - corrected_columns_to_extract - ).get('list') - corrected_final_data_cordeiro = corrected_final_data_cordeiro_ee.getInfo() - print("Corrected - Cordeiro", corrected_final_data_cordeiro) - # If no data point for this duration, then skip - if len(uncorrected_final_data) == 0: - continue - # Create pandas dataframes with the extracted information and merge them - uncorrected_df = pd.DataFrame(uncorrected_final_data, columns=uncorrected_columns_to_extract) - corrected_cordeiro_df = pd.DataFrame(corrected_final_data_cordeiro, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_cordeiro'}, axis=1) - df = pd.merge(uncorrected_df, corrected_cordeiro_df, 'left', 'to_date') - - df['from_date'] = pd.to_datetime(df['from_date'], format="%Y-%m-%d") - df['to_date'] = pd.to_datetime(df['to_date'], format="%Y-%m-%d") - df['mosaic_enddate'] = df['to_date'] - pd.Timedelta(1, unit='day') - df = df.set_index('mosaic_enddate') - print(df.head(2)) - # Save the dataframe on the disk - fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") - df.to_csv(fname) - # Create a randonm sleep time - s_time = randint(20, 30) - print(f"Sleeping for {s_time} seconds") - time.sleep(randint(20, 30)) - - if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: - print(f"Quitting: Reached enddate {enddate}") - break - elif df.index[-1].strftime('%Y-%m-%d') == fo: - print(f"Reached last available observation - {fo}") - break + # For each smaller array of dates + for subset_dates in grouped_dates: + try: + print(subset_dates) + # Convert dates list to earth engine object + dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) + # Generate Timeseries of one image corresponding to each date with water area in its attributes + res = generate_timeseries(dates).filterMetadata('l7_images', 'greater_than', 0) + # Extracting uncorrected water area and other information from attributes + uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l7_images', + 'water_red_sum', 'water_green_sum', 'water_nir_sum','water_red_green_mean','water_nir_red_mean'] + uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') + uncorrected_final_data = uncorrected_final_data_ee.getInfo() + print("Uncorrected", uncorrected_final_data) + # Extracting corrected area after corrrecting for cloud covered pixels using zhao gao correction. + res_corrected_cordeiro = res.map(lambda im: postprocess_wrapper(im, 'water_map_cordeiro', im.get('water_area_cordeiro'))) + corrected_columns_to_extract = ['to_date', 'corrected_area'] + corrected_final_data_cordeiro_ee = res_corrected_cordeiro \ + .filterMetadata('corrected_area', 'not_equals', None) \ + .reduceColumns( + ee.Reducer.toList( + len(corrected_columns_to_extract)), + corrected_columns_to_extract + ).get('list') + corrected_final_data_cordeiro = corrected_final_data_cordeiro_ee.getInfo() + print("Corrected - Cordeiro", corrected_final_data_cordeiro) + # If no data point for this duration, then skip + if len(uncorrected_final_data) == 0: + continue + # Create pandas dataframes with the extracted information and merge them + uncorrected_df = pd.DataFrame(uncorrected_final_data, columns=uncorrected_columns_to_extract) + corrected_cordeiro_df = pd.DataFrame(corrected_final_data_cordeiro, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_cordeiro'}, axis=1) + df = pd.merge(uncorrected_df, corrected_cordeiro_df, 'left', 'to_date') + + df['from_date'] = pd.to_datetime(df['from_date'], format="%Y-%m-%d") + df['to_date'] = pd.to_datetime(df['to_date'], format="%Y-%m-%d") + df['mosaic_enddate'] = df['to_date'] - pd.Timedelta(1, unit='day') + df = df.set_index('mosaic_enddate') + print(df.head(2)) + # Save the dataframe on the disk + fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") + df.to_csv(fname) + # Create a randonm sleep time + s_time = randint(20, 30) + print(f"Sleeping for {s_time} seconds") + time.sleep(randint(20, 30)) + + if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: + print(f"Quitting: Reached enddate {enddate}") + break + elif df.index[-1].strftime('%Y-%m-%d') == fo: + print(f"Reached last available observation - {fo}") + break + except Exception as e: + log.error(e) + # Adjust results_per_iter only if error includes "Too many concurrent aggregations" + if "Too many concurrent aggregations" in str(e): + results_per_iter -= 1 + print(f"Reducing Results per iteration to {results_per_iter} due to error.") + if results_per_iter < MIN_RESULTS_PER_ITER: + print("Minimum Results per iteration reached. Continuing to next group of dates.") + results_per_iter = MIN_RESULTS_PER_ITER + continue + else: + raise Exception(f'Reducing Results per iteration to {results_per_iter}.') + else: + continue + # This exception will be only raised if the error is "Too many concurrent aggregations". + # and Results per iteration will be reduced but still be greater than or equal to minimum results per iteration. + # We will continue while loop and for loop within while loop from the left over grouped dates. except Exception as e: - print(e) - # log.error(e) + dates = pd.date_range(subset_dates[0], enddate, freq=f'{TEMPORAL_RESOLUTION}D') + grouped_dates = grouper(dates, results_per_iter) continue - + # In case no exception is raised and the complete for loop ran succesfully, break the while loop + # because we need to run the for loop only once. + else: + break + # Combine the files into one database to_combine.extend([os.path.join(savedir, f) for f in os.listdir(savedir) if f.endswith(".csv")]) if len(to_combine): diff --git a/src/rat/core/sarea/sarea_cli_l8.py b/src/rat/core/sarea/sarea_cli_l8.py index 43e75657..47d609f0 100644 --- a/src/rat/core/sarea/sarea_cli_l8.py +++ b/src/rat/core/sarea/sarea_cli_l8.py @@ -49,6 +49,7 @@ def grouper(iterable, n, *, incomplete='fill', fillvalue=None): end_date = ee.Date('2019-02-01') TEMPORAL_RESOLUTION = 16 RESULTS_PER_ITER = 5 +MIN_RESULTS_PER_ITER = 1 QUALITY_PIXEL_BAND_NAME = 'QA_PIXEL' BLUE_BAND_NAME = 'SR_B2' GREEN_BAND_NAME = 'SR_B3' @@ -416,7 +417,7 @@ def get_first_obs(start_date, end_date): str_fmt = 'YYYY-MM-dd' return ee.Date.parse(str_fmt, ee.Date(first_im.get('system:time_start')).format(str_fmt)) -def run_process_long(res_name, res_polygon, start, end, datadir): +def run_process_long(res_name, res_polygon, start, end, datadir, results_per_iter=RESULTS_PER_ITER): fo = start enddate = end @@ -462,88 +463,102 @@ def run_process_long(res_name, res_polygon, start, end, datadir): print(f"Extracting SA for the period {fo} -> {enddate}") dates = pd.date_range(fo, enddate, freq=f'{TEMPORAL_RESOLUTION}D') - grouped_dates = grouper(dates, RESULTS_PER_ITER) + grouped_dates = grouper(dates, results_per_iter) - # # redo the calculations part and see where it is complaining about too many aggregations - # subset_dates = next(grouped_dates) - # dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) - - # print(subset_dates) - # res = generate_timeseries(dates).filterMetadata('s2_images', 'greater_than', 0) - # pprint.pprint(res.aggregate_array('s2_images').getInfo()) - - # uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'water_area_NDWI', 'non_water_area_NDWI', 'cloud_area', 's2_images'] - # uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') - # uncorrected_final_data = uncorrected_final_data_ee.getInfo() - - for subset_dates in grouped_dates: + # Until results per iteration is less than min results per iteration + while results_per_iter >= MIN_RESULTS_PER_ITER: + # try to run for each subset of dates try: - print(subset_dates) - dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) - - res = generate_timeseries(dates).filterMetadata('l8_images', 'greater_than', 0) - # pprint.pprint(res.getInfo()) - - uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l8_images', - 'water_red_sum', 'water_green_sum', 'water_nir_sum','water_red_green_mean','water_nir_red_mean'] - uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') - uncorrected_final_data = uncorrected_final_data_ee.getInfo() - print("Uncorrected", uncorrected_final_data) - - res_corrected_cordeiro = res.map(lambda im: postprocess_wrapper(im, 'water_map_cordeiro', im.get('water_area_cordeiro'))) - corrected_columns_to_extract = ['to_date', 'corrected_area'] - corrected_final_data_cordeiro_ee = res_corrected_cordeiro \ - .filterMetadata('corrected_area', 'not_equals', None) \ - .reduceColumns( - ee.Reducer.toList( - len(corrected_columns_to_extract)), - corrected_columns_to_extract - ).get('list') - corrected_final_data_cordeiro = corrected_final_data_cordeiro_ee.getInfo() - print("Corrected - Cordeiro", corrected_final_data_cordeiro) - - # res_corrected_NDWI = res.map(lambda im: postprocess_wrapper(im, 'water_map_NDWI', im.get('water_area_NDWI'))) - # corrected_final_data_NDWI_ee = res_corrected_NDWI \ - # .filterMetadata('corrected_area', 'not_equals', None) \ - # .reduceColumns( - # ee.Reducer.toList( - # len(corrected_columns_to_extract)), - # corrected_columns_to_extract - # ).get('list') - - # corrected_final_data_NDWI = corrected_final_data_NDWI_ee.getInfo() - - # print(uncorrected_final_data, corrected_final_data_cordeiro) - if len(uncorrected_final_data) == 0: - continue - uncorrected_df = pd.DataFrame(uncorrected_final_data, columns=uncorrected_columns_to_extract) - corrected_cordeiro_df = pd.DataFrame(corrected_final_data_cordeiro, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_cordeiro'}, axis=1) - # corrected_NDWI_df = pd.DataFrame(corrected_final_data_NDWI, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_NDWI'}, axis=1) - # corrected_df = pd.merge(corrected_cordeiro_df, corrected_NDWI_df, 'left', 'to_date') - df = pd.merge(uncorrected_df, corrected_cordeiro_df, 'left', 'to_date') - - df['from_date'] = pd.to_datetime(df['from_date'], format="%Y-%m-%d") - df['to_date'] = pd.to_datetime(df['to_date'], format="%Y-%m-%d") - df['mosaic_enddate'] = df['to_date'] - pd.Timedelta(1, unit='day') - df = df.set_index('mosaic_enddate') - print(df.head(2)) - - fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") - df.to_csv(fname) - - s_time = randint(20, 30) - print(f"Sleeping for {s_time} seconds") - time.sleep(randint(20, 30)) - - if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: - print(f"Quitting: Reached enddate {enddate}") - break - elif df.index[-1].strftime('%Y-%m-%d') == fo: - print(f"Reached last available observation - {fo}") - break + for subset_dates in grouped_dates: + try: + print(subset_dates) + dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) + + res = generate_timeseries(dates).filterMetadata('l8_images', 'greater_than', 0) + # pprint.pprint(res.getInfo()) + + uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l8_images', + 'water_red_sum', 'water_green_sum', 'water_nir_sum','water_red_green_mean','water_nir_red_mean'] + uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') + uncorrected_final_data = uncorrected_final_data_ee.getInfo() + print("Uncorrected", uncorrected_final_data) + + res_corrected_cordeiro = res.map(lambda im: postprocess_wrapper(im, 'water_map_cordeiro', im.get('water_area_cordeiro'))) + corrected_columns_to_extract = ['to_date', 'corrected_area'] + corrected_final_data_cordeiro_ee = res_corrected_cordeiro \ + .filterMetadata('corrected_area', 'not_equals', None) \ + .reduceColumns( + ee.Reducer.toList( + len(corrected_columns_to_extract)), + corrected_columns_to_extract + ).get('list') + corrected_final_data_cordeiro = corrected_final_data_cordeiro_ee.getInfo() + print("Corrected - Cordeiro", corrected_final_data_cordeiro) + + # res_corrected_NDWI = res.map(lambda im: postprocess_wrapper(im, 'water_map_NDWI', im.get('water_area_NDWI'))) + # corrected_final_data_NDWI_ee = res_corrected_NDWI \ + # .filterMetadata('corrected_area', 'not_equals', None) \ + # .reduceColumns( + # ee.Reducer.toList( + # len(corrected_columns_to_extract)), + # corrected_columns_to_extract + # ).get('list') + + # corrected_final_data_NDWI = corrected_final_data_NDWI_ee.getInfo() + + # print(uncorrected_final_data, corrected_final_data_cordeiro) + if len(uncorrected_final_data) == 0: + continue + uncorrected_df = pd.DataFrame(uncorrected_final_data, columns=uncorrected_columns_to_extract) + corrected_cordeiro_df = pd.DataFrame(corrected_final_data_cordeiro, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_cordeiro'}, axis=1) + # corrected_NDWI_df = pd.DataFrame(corrected_final_data_NDWI, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_NDWI'}, axis=1) + # corrected_df = pd.merge(corrected_cordeiro_df, corrected_NDWI_df, 'left', 'to_date') + df = pd.merge(uncorrected_df, corrected_cordeiro_df, 'left', 'to_date') + + df['from_date'] = pd.to_datetime(df['from_date'], format="%Y-%m-%d") + df['to_date'] = pd.to_datetime(df['to_date'], format="%Y-%m-%d") + df['mosaic_enddate'] = df['to_date'] - pd.Timedelta(1, unit='day') + df = df.set_index('mosaic_enddate') + print(df.head(2)) + + fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") + df.to_csv(fname) + + s_time = randint(20, 30) + print(f"Sleeping for {s_time} seconds") + time.sleep(randint(20, 30)) + + if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: + print(f"Quitting: Reached enddate {enddate}") + break + elif df.index[-1].strftime('%Y-%m-%d') == fo: + print(f"Reached last available observation - {fo}") + break + except Exception as e: + log.error(e) + # Adjust results_per_iter only if error includes "Too many concurrent aggregations" + if "Too many concurrent aggregations" in str(e): + results_per_iter -= 1 + print(f"Reducing Results per iteration to {results_per_iter} due to error.") + if results_per_iter < MIN_RESULTS_PER_ITER: + print("Minimum Results per iteration reached. Continuing to next group of dates.") + results_per_iter = MIN_RESULTS_PER_ITER + continue + else: + raise Exception(f'Reducing Results per iteration to {results_per_iter}.') + else: + continue + # This exception will be only raised if the error is "Too many concurrent aggregations". + # and Results per iteration will be reduced but still be greater than or equal to minimum results per iteration. + # We will continue while loop and for loop within while loop from the left over grouped dates. except Exception as e: - log.error(e) + dates = pd.date_range(subset_dates[0], enddate, freq=f'{TEMPORAL_RESOLUTION}D') + grouped_dates = grouper(dates, results_per_iter) continue + # In case no exception is raised and the complete for loop ran succesfully, break the while loop + # because we need to run the for loop only once. + else: + break diff --git a/src/rat/core/sarea/sarea_cli_l9.py b/src/rat/core/sarea/sarea_cli_l9.py index 95313b05..e91b7ea7 100644 --- a/src/rat/core/sarea/sarea_cli_l9.py +++ b/src/rat/core/sarea/sarea_cli_l9.py @@ -51,6 +51,7 @@ def grouper(iterable, n, *, incomplete='fill', fillvalue=None): end_date = ee.Date('2019-02-01') TEMPORAL_RESOLUTION = 16 RESULTS_PER_ITER = 5 +MIN_RESULTS_PER_ITER = 1 MISSION_START_DATE = (2022,1,1) # Rough start date for mission/satellite data QUALITY_PIXEL_BAND_NAME = 'QA_PIXEL' BLUE_BAND_NAME = 'SR_B2' @@ -418,7 +419,7 @@ def get_first_obs(start_date, end_date): str_fmt = 'YYYY-MM-dd' return ee.Date.parse(str_fmt, ee.Date(first_im.get('system:time_start')).format(str_fmt)) -def run_process_long(res_name, res_polygon, start, end, datadir): +def run_process_long(res_name, res_polygon, start, end, datadir, results_per_iter=RESULTS_PER_ITER): fo = start enddate = end @@ -468,89 +469,102 @@ def run_process_long(res_name, res_polygon, start, end, datadir): print(f"Extracting SA for the period {fo} -> {enddate}") dates = pd.date_range(fo, enddate, freq=f'{TEMPORAL_RESOLUTION}D') - grouped_dates = grouper(dates, RESULTS_PER_ITER) - - # # redo the calculations part and see where it is complaining about too many aggregations - # subset_dates = next(grouped_dates) - # dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) - - # print(subset_dates) - # res = generate_timeseries(dates).filterMetadata('s2_images', 'greater_than', 0) - # pprint.pprint(res.aggregate_array('s2_images').getInfo()) - - # uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'water_area_NDWI', 'non_water_area_NDWI', 'cloud_area', 's2_images'] - # uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') - # uncorrected_final_data = uncorrected_final_data_ee.getInfo() + grouped_dates = grouper(dates, results_per_iter) - for subset_dates in grouped_dates: + # Until results per iteration is less than min results per iteration + while results_per_iter >= MIN_RESULTS_PER_ITER: + # try to run for each subset of dates try: - print(subset_dates) - dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) - - res = generate_timeseries(dates).filterMetadata('l9_images', 'greater_than', 0) - # pprint.pprint(res.getInfo()) - - uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l9_images', - 'water_red_sum', 'water_green_sum', 'water_nir_sum','water_red_green_mean','water_nir_red_mean'] - uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') - uncorrected_final_data = uncorrected_final_data_ee.getInfo() - print("Uncorrected", uncorrected_final_data) - - res_corrected_cordeiro = res.map(lambda im: postprocess_wrapper(im, 'water_map_cordeiro', im.get('water_area_cordeiro'))) - corrected_columns_to_extract = ['to_date', 'corrected_area'] - corrected_final_data_cordeiro_ee = res_corrected_cordeiro \ - .filterMetadata('corrected_area', 'not_equals', None) \ - .reduceColumns( - ee.Reducer.toList( - len(corrected_columns_to_extract)), - corrected_columns_to_extract - ).get('list') - corrected_final_data_cordeiro = corrected_final_data_cordeiro_ee.getInfo() - print("Corrected - Cordeiro", corrected_final_data_cordeiro) - - # res_corrected_NDWI = res.map(lambda im: postprocess_wrapper(im, 'water_map_NDWI', im.get('water_area_NDWI'))) - # corrected_final_data_NDWI_ee = res_corrected_NDWI \ - # .filterMetadata('corrected_area', 'not_equals', None) \ - # .reduceColumns( - # ee.Reducer.toList( - # len(corrected_columns_to_extract)), - # corrected_columns_to_extract - # ).get('list') - - # corrected_final_data_NDWI = corrected_final_data_NDWI_ee.getInfo() - - # print(uncorrected_final_data, corrected_final_data_cordeiro) - if len(uncorrected_final_data) == 0: - continue - uncorrected_df = pd.DataFrame(uncorrected_final_data, columns=uncorrected_columns_to_extract) - corrected_cordeiro_df = pd.DataFrame(corrected_final_data_cordeiro, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_cordeiro'}, axis=1) - # corrected_NDWI_df = pd.DataFrame(corrected_final_data_NDWI, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_NDWI'}, axis=1) - # corrected_df = pd.merge(corrected_cordeiro_df, corrected_NDWI_df, 'left', 'to_date') - df = pd.merge(uncorrected_df, corrected_cordeiro_df, 'left', 'to_date') - - df['from_date'] = pd.to_datetime(df['from_date'], format="%Y-%m-%d") - df['to_date'] = pd.to_datetime(df['to_date'], format="%Y-%m-%d") - df['mosaic_enddate'] = df['to_date'] - pd.Timedelta(1, unit='day') - df = df.set_index('mosaic_enddate') - print(df.head(2)) - - fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") - df.to_csv(fname) - - s_time = randint(20, 30) - print(f"Sleeping for {s_time} seconds") - time.sleep(randint(20, 30)) - - if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: - print(f"Quitting: Reached enddate {enddate}") - break - elif df.index[-1].strftime('%Y-%m-%d') == fo: - print(f"Reached last available observation - {fo}") - break + for subset_dates in grouped_dates: + try: + print(subset_dates) + dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) + + res = generate_timeseries(dates).filterMetadata('l9_images', 'greater_than', 0) + # pprint.pprint(res.getInfo()) + + uncorrected_columns_to_extract = ['from_date', 'to_date', 'water_area_cordeiro', 'non_water_area_cordeiro', 'cloud_area', 'l9_images', + 'water_red_sum', 'water_green_sum', 'water_nir_sum','water_red_green_mean','water_nir_red_mean'] + uncorrected_final_data_ee = res.reduceColumns(ee.Reducer.toList(len(uncorrected_columns_to_extract)), uncorrected_columns_to_extract).get('list') + uncorrected_final_data = uncorrected_final_data_ee.getInfo() + print("Uncorrected", uncorrected_final_data) + + res_corrected_cordeiro = res.map(lambda im: postprocess_wrapper(im, 'water_map_cordeiro', im.get('water_area_cordeiro'))) + corrected_columns_to_extract = ['to_date', 'corrected_area'] + corrected_final_data_cordeiro_ee = res_corrected_cordeiro \ + .filterMetadata('corrected_area', 'not_equals', None) \ + .reduceColumns( + ee.Reducer.toList( + len(corrected_columns_to_extract)), + corrected_columns_to_extract + ).get('list') + corrected_final_data_cordeiro = corrected_final_data_cordeiro_ee.getInfo() + print("Corrected - Cordeiro", corrected_final_data_cordeiro) + + # res_corrected_NDWI = res.map(lambda im: postprocess_wrapper(im, 'water_map_NDWI', im.get('water_area_NDWI'))) + # corrected_final_data_NDWI_ee = res_corrected_NDWI \ + # .filterMetadata('corrected_area', 'not_equals', None) \ + # .reduceColumns( + # ee.Reducer.toList( + # len(corrected_columns_to_extract)), + # corrected_columns_to_extract + # ).get('list') + + # corrected_final_data_NDWI = corrected_final_data_NDWI_ee.getInfo() + + # print(uncorrected_final_data, corrected_final_data_cordeiro) + if len(uncorrected_final_data) == 0: + continue + uncorrected_df = pd.DataFrame(uncorrected_final_data, columns=uncorrected_columns_to_extract) + corrected_cordeiro_df = pd.DataFrame(corrected_final_data_cordeiro, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_cordeiro'}, axis=1) + # corrected_NDWI_df = pd.DataFrame(corrected_final_data_NDWI, columns=corrected_columns_to_extract).rename({'corrected_area': 'corrected_area_NDWI'}, axis=1) + # corrected_df = pd.merge(corrected_cordeiro_df, corrected_NDWI_df, 'left', 'to_date') + df = pd.merge(uncorrected_df, corrected_cordeiro_df, 'left', 'to_date') + + df['from_date'] = pd.to_datetime(df['from_date'], format="%Y-%m-%d") + df['to_date'] = pd.to_datetime(df['to_date'], format="%Y-%m-%d") + df['mosaic_enddate'] = df['to_date'] - pd.Timedelta(1, unit='day') + df = df.set_index('mosaic_enddate') + print(df.head(2)) + + fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") + df.to_csv(fname) + + s_time = randint(20, 30) + print(f"Sleeping for {s_time} seconds") + time.sleep(randint(20, 30)) + + if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: + print(f"Quitting: Reached enddate {enddate}") + break + elif df.index[-1].strftime('%Y-%m-%d') == fo: + print(f"Reached last available observation - {fo}") + break + except Exception as e: + # Adjust results_per_iter only if error includes "Too many concurrent aggregations" + if "Too many concurrent aggregations" in str(e): + results_per_iter -= 1 + print(f"Reducing Results per iteration to {results_per_iter} due to error.") + if results_per_iter < MIN_RESULTS_PER_ITER: + print("Minimum Results per iteration reached. Continuing to next group of dates.") + results_per_iter = MIN_RESULTS_PER_ITER + continue + else: + raise Exception(f'Reducing Results per iteration to {results_per_iter}.') + else: + continue + # This exception will be only raised if the error is "Too many concurrent aggregations". + # and Results per iteration will be reduced but still be greater than or equal to minimum results per iteration. + # We will continue while loop and for loop within while loop from the left over grouped dates. except Exception as e: - log.error(e) + dates = pd.date_range(subset_dates[0], enddate, freq=f'{TEMPORAL_RESOLUTION}D') + grouped_dates = grouper(dates, results_per_iter) continue - + # In case no exception is raised and the complete for loop ran succesfully, break the while loop + # because we need to run the for loop only once. + else: + break + # Combine the files into one database From d6818537222864ae3047d36245b6e6df3e195c0d Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 30 Jan 2025 03:24:49 -0800 Subject: [PATCH 055/102] Error handling for the error computation timed out & added check to process dates only after mission start date for sentinel-1 --- src/rat/core/sarea/sarea_cli_sar.py | 69 ++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_sar.py b/src/rat/core/sarea/sarea_cli_sar.py index c1f29934..90f080bf 100644 --- a/src/rat/core/sarea/sarea_cli_sar.py +++ b/src/rat/core/sarea/sarea_cli_sar.py @@ -2,11 +2,16 @@ import numpy as np import pandas as pd import os -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date +from logging import getLogger from rat.core.sarea.sarea_cli_s2 import TEMPORAL_RESOLUTION from rat.ee_utils.ee_utils import poly2feature from rat.utils.utils import days_between +from rat.utils.logging import LOG_NAME, NOTIFICATION + + +log = getLogger(f"{LOG_NAME}.{__name__}") s1 = ee.ImageCollection("COPERNICUS/S1_GRD") @@ -15,7 +20,10 @@ ANGLE_THRESHOLD_2 = ee.Number(31.66) REVISIT_TIME = ee.Number(12) BUFFER_DIST = 500 - +SPATIAL_SCALE_SMALL = 10 +SPATIAL_SCALE_MEDIUM = 30 +SPATIAL_SCALE_LARGE = 50 +MISSION_START_DATE = date(2014,1,1) # Rough start date for mission/satellite data # functions def getfirstobs(imcoll): @@ -42,10 +50,10 @@ def mask_by_angle(img): return vv.updateMask(mask2) # Calculating the water pixels -def calcWaterPix(img): +def calcWaterPix(img, scale): water_pixels = ee.Algorithms.If( img.bandNames().contains('Class'), - img.reduceRegion(reducer = ee.Reducer.sum(), geometry = ROI, scale = 10, maxPixels = 10e9).get('Class'), + img.reduceRegion(reducer = ee.Reducer.sum(), geometry = ROI, scale = scale, maxPixels = 10e9).get('Class'), None) return img.set("water_pixels", water_pixels) @@ -76,7 +84,7 @@ def in_case_we_have_obs(): return res # client side code -def ee_get_data(ee_Date_Start, ee_Date_End): +def ee_get_data(ee_Date_Start, ee_Date_End, spatial_scale): date_start_str = ee_Date_Start date_end_str = ee_Date_End ee_Date_Start, ee_Date_End = ee.Date(ee_Date_Start), ee.Date(ee_Date_End) @@ -85,8 +93,6 @@ def ee_get_data(ee_Date_Start, ee_Date_End): .filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VV')) \ .filter(ee.Filter.eq('instrumentMode', 'IW')) - print(S1.size().getInfo()) - ## Checking that the image collection should not be empty in GEE ## Not checking for only cases where time interval is small ## because SAR images in GEE are irregularly present. So there can be large such intervals as well. @@ -101,12 +107,12 @@ def ee_get_data(ee_Date_Start, ee_Date_End): dates = dates.map(lambda n: first_date.advance(n, 'day')) classified_water_sar = ee.ImageCollection(dates.map(lambda d: detectWaterSAR(d, ref_image))) - classified_water_sar = classified_water_sar.map(calcWaterPix) + classified_water_sar = classified_water_sar.map(lambda img: calcWaterPix(img, scale=spatial_scale)) # print('size line 103:',classified_water_sar.size().getInfo()) # print('size line 104:',ee.Array(classified_water_sar.aggregate_array('water_pixels')).multiply(0.0001).size().getInfo()) - wc = ee.Array(classified_water_sar.aggregate_array('water_pixels')).multiply(0.0001).getInfo() # area in sq. km + wc = ee.Array(classified_water_sar.aggregate_array('water_pixels')).multiply(spatial_scale).multiply(spatial_scale).divide(1000000).getInfo() # area in sq. km d = classified_water_sar.aggregate_array('system:time_start').getInfo() # convert from miliseconds to seconds from epoch df = pd.DataFrame({ @@ -125,16 +131,49 @@ def ee_get_data(ee_Date_Start, ee_Date_End): return df def retrieve_sar(start_date, end_date, res='ys'): #ys-year-start frequency - date_ranges = list((pd.date_range(start_date, end_date, freq=res).union([pd.to_datetime(start_date), pd.to_datetime(end_date)])).strftime("%Y-%m-%d").tolist()) #ys-year-start frequency + # Ensure start_date is not before MISSION_START_DATE + print(f'Sentinel 1 mission start date is roughly: {MISSION_START_DATE}. Ensuring start date to extract surface area is greater than or equal to it.') + start_date = max(pd.to_datetime(start_date), pd.to_datetime(MISSION_START_DATE)) + + # Generate date range, ensuring both start and end dates are included + date_ranges = pd.date_range(start_date, end_date, freq=res).union( + [pd.to_datetime(start_date), pd.to_datetime(end_date)] + ).strftime("%Y-%m-%d").tolist() + print(date_ranges) dfs = [] + scale_to_use = SPATIAL_SCALE_SMALL # for begin, end in zip(date_ranges[:-1], date_ranges.shift(1)[:-1]): for begin, end in zip(date_ranges[:-1], date_ranges[1:]): - # begin_str, end_str = begin.strftime('%Y-%m-%d'), end.strftime('%Y-%m-%d') - print(f"Processing: {begin} - {end} ") - dfs.append(ee_get_data(begin, end)) - print(dfs[-1].head()) - print(f"Processed: {begin} - {end} ") + success_status = 0 # initialize success_status with 0 which will remain 0 on fail attempt and become 1 on successful attempt + while (success_status == 0): + try: + # begin_str, end_str = begin.strftime('%Y-%m-%d'), end.strftime('%Y-%m-%d') + print(f"Processing: {begin} - {end} ") + dfs.append(ee_get_data(begin, end, spatial_scale=scale_to_use)) + except Exception as e: + log.error(e) + # Adjust results_per_iter only if error includes "Too many concurrent aggregations" + if "Computation timed out" in str(e): + if scale_to_use == SPATIAL_SCALE_SMALL: + scale_to_use = SPATIAL_SCALE_MEDIUM + print(f"Trying with larger spatial resolution: {scale_to_use} m.") + success_status = 0 + continue + elif scale_to_use == SPATIAL_SCALE_MEDIUM: + scale_to_use = SPATIAL_SCALE_LARGE + print(f"Trying with larger spatial resolution: {scale_to_use} m.") + success_status = 0 + continue + else: + print("Trying with larger spatial resolution failed. Moving to next iteration.") + scale_to_use = SPATIAL_SCALE_MEDIUM + success_status = 1 + break + else: + success_status = 1 + print(dfs[-1].head()) + print(f"Processed: {begin} - {end} ") return pd.concat(dfs) From bfb596ab3cd5384c6c8475b2b3642cf1588b06f8 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 30 Jan 2025 03:35:09 -0800 Subject: [PATCH 056/102] Bug Fix: Corrected error handling of NSSC calculation, separated from dels --- src/rat/core/run_postprocessing.py | 38 +++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/rat/core/run_postprocessing.py b/src/rat/core/run_postprocessing.py index ae73ecd6..25f489a8 100644 --- a/src/rat/core/run_postprocessing.py +++ b/src/rat/core/run_postprocessing.py @@ -218,10 +218,9 @@ def run_postprocessing(basin_name, basin_data_dir, reservoir_shpfile, reservoir_ # SArea sarea_raw_dir = os.path.join(basin_data_dir,'gee', "gee_sarea_tmsos") - nssc_raw_dir = os.path.join(basin_data_dir,'gee', "gee_nssc") ## No of failed files (no_failed_files) is tracked and used to print a warning message in log level 1 file. - # DelS calculation & copying of NSSC files + # DelS calculation if(gee_status): log.debug("Calculating ∆S") no_failed_files = 0 @@ -232,9 +231,7 @@ def run_postprocessing(basin_name, basin_data_dir, reservoir_shpfile, reservoir_ # Reading reservoir information reservoir_name = str(reservoir[reservoir_shpfile_column_dict['unique_identifier']]) sarea_path = os.path.join(sarea_raw_dir, reservoir_name + ".csv") - nssc_path = os.path.join(nssc_raw_dir, reservoir_name + ".csv") dels_savepath = os.path.join(dels_savedir, reservoir_name + ".csv") - nssc_savepath = os.path.join(nssc_savedir, reservoir_name + ".csv") aecpath = os.path.join(aec_dir, reservoir_name + ".csv") if os.path.isfile(sarea_path): @@ -243,20 +240,43 @@ def run_postprocessing(basin_name, basin_data_dir, reservoir_shpfile, reservoir_ else: raise Exception("Surface area file not found; skipping ∆S calculation") + except: + log.exception(f"∆S for {reservoir_name} could not be calculated.") + no_failed_files += 1 + DELS_STATUS=1 + if no_failed_files: + log_level1.warning(f"∆S was not calculated for {no_failed_files} reservoir(s). Please check Level-2 log file for more details.") + else: + log.debug("Cannot Calculate ∆S because GEE Run Failed.") + + # NSSC + nssc_raw_dir = os.path.join(basin_data_dir,'gee', "gee_nssc") + # copying of NSSC files + if(gee_status): + log.debug("Copying NSSC data to RAT outputs.") + no_failed_files = 0 + + for reservoir_no,reservoir in reservoirs.iterrows(): + try: + # Reading reservoir information + reservoir_name = str(reservoir[reservoir_shpfile_column_dict['unique_identifier']]) + nssc_path = os.path.join(nssc_raw_dir, reservoir_name + ".csv") + nssc_savepath = os.path.join(nssc_savedir, reservoir_name + ".csv") + if os.path.isfile(nssc_path): nssc_df = pd.read_csv(nssc_path) nssc_df.to_csv(nssc_savepath, index=False) else: - raise Exception("NSSC file not found for {reservoir_name}; skipping copy to RAT Outputs") + raise Exception(f"NSSC file not found for {reservoir_name}; skipping copy to RAT Outputs") + except: - log.exception(f"∆S for {reservoir_name} could not be calculated.") + log.exception(f"NSSC for {reservoir_name} could not be calculated.") no_failed_files += 1 DELS_STATUS=1 if no_failed_files: - log_level1.warning(f"∆S was not calculated for {no_failed_files} reservoir(s). Please check Level-2 log file for more details.") + log_level1.warning(f"NSSC was not calculated for {no_failed_files} reservoir(s). Please check Level-2 log file for more details.") else: - log.debug("Cannot Calculate ∆S because GEE Run Failed.") - + log.debug("Cannot Calculate NSSC because GEE Run Failed.") # Evaporation if(vic_status and gee_status): From 8b0f090efb9eb7ee6c510fdd7f5a85714a30438f Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 30 Jan 2025 04:01:47 -0800 Subject: [PATCH 057/102] Bug Fix: corrected import of plugins module for forecasting & improved logging info --- src/rat/run_rat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rat/run_rat.py b/src/rat/run_rat.py index 386892bb..846c20b6 100644 --- a/src/rat/run_rat.py +++ b/src/rat/run_rat.py @@ -39,8 +39,7 @@ def run_rat(config_fn, operational_latency=None ): # Logging this run log_dir = os.path.join(config['GLOBAL']['data_dir'],'runs','logs','') - print(f"Logging this run at {log_dir}") - log = init_logger( + log, log_file_path = init_logger( log_dir, verbose=False, # notify=True, @@ -49,6 +48,7 @@ def run_rat(config_fn, operational_latency=None ): logger_name=LOG_LEVEL1_NAME, for_basin=False ) + print(f"Logging this run at {log_file_path}") log.debug("Initiating Dask Client ... ") cluster = LocalCluster(name="RAT", n_workers=config['GLOBAL']['multiprocessing'], threads_per_worker=1) @@ -298,7 +298,7 @@ def run_rat(config_fn, operational_latency=None ): if config.get('PLUGINS', {}).get('forecasting'): # Importing the forecast module try: - from plugins.forecasting.forecast_basin import forecast + from rat.plugins.forecasting.forecast_basin import forecast except: log.exception("Failed to import Forecast plugin due to missing package(s).") log.info('############## Starting RAT forecast for '+config['BASIN']['basin_name']+' #################') From 97cc801b9eb7a3b00a60d9f90f453494a4d3989d Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 30 Jan 2025 04:02:10 -0800 Subject: [PATCH 058/102] Added level-2 log path in level-1 log file. --- src/rat/rat_basin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rat/rat_basin.py b/src/rat/rat_basin.py index 3ab6ac10..c61c09e9 100644 --- a/src/rat/rat_basin.py +++ b/src/rat/rat_basin.py @@ -187,7 +187,7 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base # Defining logger - log = init_logger( + log, log_file_path = init_logger( log_dir= log_dir, verbose= False, # notify= True, @@ -225,6 +225,7 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base rat_logger.info(f"Running RAT from {config['BASIN']['start'] } to {config['BASIN']['end']} which includes spin-up.") else: rat_logger.info(f"Running RAT from {config['BASIN']['start'] } to {config['BASIN']['end']}.") + rat_logger.info(f"Level-2 (DETAILED) log for this River Basin is at {log_file_path}") if gfs_days: rat_logger.info(f"Note 1: Due to low latency availability, GFS daily forecasted data will be used for {gfs_days} most recent days.") rat_logger.info(f"Note 2: The GFS data will be removed and replaced in next RAT run by observed data, if available.") From 8fe1dc2389cc55d9649d2e91b652b5d93890c5b8 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 30 Jan 2025 04:02:44 -0800 Subject: [PATCH 059/102] added log mode and log detail levels in log files --- src/rat/utils/logging.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/rat/utils/logging.py b/src/rat/utils/logging.py index 5146a319..0ea0a998 100644 --- a/src/rat/utils/logging.py +++ b/src/rat/utils/logging.py @@ -83,9 +83,13 @@ def init_logger(log_dir='./', log_level='DEBUG', verbose=False, notify=False, lo if(for_basin): log_file = os.path.join(log_dir, 'RAT-'+ basin_name + strftime('%Y%m%d-%H%M%S', gmtime()) + '.log') + log_detail = 'LEVEL 2' + log_mode = 'DETAILED' else: log_file = os.path.join(log_dir, 'RAT_run-'+ strftime('%Y%m%d-%H%M%S', gmtime()) + '.log') + log_detail = 'LEVEL 1' + log_mode = 'BRIEF' fh = logging.FileHandler(log_file) fh.setLevel(log_level) fh.setFormatter(FORMATTER) @@ -122,13 +126,15 @@ def init_logger(log_dir='./', log_level='DEBUG', verbose=False, notify=False, lo logger.info('-------------------- INITIALIZED RAT-'+basin_name+' LOG ------------------') logger.info('TIME: %s', datetime.datetime.now()) + logger.info('LOG DETAIL: %s', log_detail) + logger.info('LOG MODE: %s', log_mode) logger.info('LOG LEVEL: %s', log_level) logger.info('Logging To Console: %s', verbose) logger.info('LOG FILE: %s', log_file) logger.info('NOTIFY: %s', notify) logger.info('----------------------------------------------------------\n') - return logger + return logger, log_file # -------------------------------------------------------------------- # From dac62f778a58e3606109fb46c8f806ea58a3eae1 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 30 Jan 2025 04:12:57 -0800 Subject: [PATCH 060/102] improved design of logs to standout the detail level and mode --- src/rat/utils/logging.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/rat/utils/logging.py b/src/rat/utils/logging.py index 0ea0a998..7f85de77 100644 --- a/src/rat/utils/logging.py +++ b/src/rat/utils/logging.py @@ -126,13 +126,15 @@ def init_logger(log_dir='./', log_level='DEBUG', verbose=False, notify=False, lo logger.info('-------------------- INITIALIZED RAT-'+basin_name+' LOG ------------------') logger.info('TIME: %s', datetime.datetime.now()) - logger.info('LOG DETAIL: %s', log_detail) - logger.info('LOG MODE: %s', log_mode) logger.info('LOG LEVEL: %s', log_level) logger.info('Logging To Console: %s', verbose) logger.info('LOG FILE: %s', log_file) logger.info('NOTIFY: %s', notify) - logger.info('----------------------------------------------------------\n') + logger.info('--------------------------------------------------------------------------') + logger.info('--------------------------------------------------------------------------') + logger.info('LOG DETAIL: %s', log_detail) + logger.info('LOG MODE: %s', log_mode) + logger.info('--------------------------------------------------------------------------\n') return logger, log_file # -------------------------------------------------------------------- # From 8eec3e8a56c4dd06350f6b07203478c4d7eb9b09 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 30 Jan 2025 04:18:12 -0800 Subject: [PATCH 061/102] improved design of logs --- src/rat/run_rat.py | 2 +- src/rat/utils/logging.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/rat/run_rat.py b/src/rat/run_rat.py index 846c20b6..64c1cf78 100644 --- a/src/rat/run_rat.py +++ b/src/rat/run_rat.py @@ -67,7 +67,7 @@ def run_rat(config_fn, operational_latency=None ): with StringIO() as fake_stderr, redirect_stderr(fake_stderr): ee_credentials = ee.ServiceAccountCredentials(ee_configuration.service_account,ee_configuration.key_file) ee.Initialize(ee_credentials) - log.info("Connected to earth engine successfully.") + log.info("Connected to earth engine successfully.\n") except Exception as e: log.error(f"Failed to connect to Earth Engine. RAT will not be able to use Surface Area Estimations. Error: {e}") finally: diff --git a/src/rat/utils/logging.py b/src/rat/utils/logging.py index 7f85de77..7b858f3b 100644 --- a/src/rat/utils/logging.py +++ b/src/rat/utils/logging.py @@ -130,11 +130,10 @@ def init_logger(log_dir='./', log_level='DEBUG', verbose=False, notify=False, lo logger.info('Logging To Console: %s', verbose) logger.info('LOG FILE: %s', log_file) logger.info('NOTIFY: %s', notify) - logger.info('--------------------------------------------------------------------------') - logger.info('--------------------------------------------------------------------------') + logger.info('----------------------------------------------------------------') logger.info('LOG DETAIL: %s', log_detail) logger.info('LOG MODE: %s', log_mode) - logger.info('--------------------------------------------------------------------------\n') + logger.info('----------------------------------------------------------------\n') return logger, log_file # -------------------------------------------------------------------- # From 3b0a08935858039447fe8cb1285ce4be6469fa92 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 30 Jan 2025 04:25:51 -0800 Subject: [PATCH 062/102] added newline after each basin run in level-1 log --- src/rat/run_rat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rat/run_rat.py b/src/rat/run_rat.py index 64c1cf78..d82a349a 100644 --- a/src/rat/run_rat.py +++ b/src/rat/run_rat.py @@ -317,11 +317,11 @@ def run_rat(config_fn, operational_latency=None ): basins_metadata['ALTIMETER','last_cycle_number'].where(basins_metadata['BASIN','basin_name']!= basin, latest_altimetry_cycle, inplace=True) basins_metadata.to_csv(config_copy['GLOBAL']['basins_metadata'], index=False) if(no_errors>0): - log.info('############## RAT run finished for '+config_copy['BASIN']['basin_name']+ ' with '+str(no_errors)+' error(s). #################') + log.info('############## RAT run finished for '+config_copy['BASIN']['basin_name']+ ' with '+str(no_errors)+' error(s). #################\n') elif(no_errors==0): - log.info('############## Successfully run RAT for '+config_copy['BASIN']['basin_name']+' #################') + log.info('############## Successfully run RAT for '+config_copy['BASIN']['basin_name']+' #################\n') else: - log.error('############## RAT run failed for '+config_copy['BASIN']['basin_name']+' #################') + log.error('############## RAT run failed for '+config_copy['BASIN']['basin_name']+' #################\n') # Closing logger close_logger('rat_run') From f683639f27390982465661f0b5beb5084f05d93f Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 2 Feb 2025 18:59:38 -0800 Subject: [PATCH 063/102] Made the function get_first_obs efficient --- src/rat/core/sarea/sarea_cli_l5.py | 17 +++++++++++++---- src/rat/core/sarea/sarea_cli_l7.py | 17 +++++++++++++---- src/rat/core/sarea/sarea_cli_l8.py | 17 +++++++++++++---- src/rat/core/sarea/sarea_cli_l9.py | 17 +++++++++++++---- src/rat/core/sarea/sarea_cli_s2.py | 17 +++++++++++++---- 5 files changed, 65 insertions(+), 20 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_l5.py b/src/rat/core/sarea/sarea_cli_l5.py index 24b0c369..00e36fbf 100644 --- a/src/rat/core/sarea/sarea_cli_l5.py +++ b/src/rat/core/sarea/sarea_cli_l5.py @@ -383,9 +383,18 @@ def generate_timeseries(dates): return imcoll def get_first_obs(start_date, end_date): - first_im = l5.filterBounds(aoi).filterDate(start_date, end_date).first() - str_fmt = 'YYYY-MM-dd' - return ee.Date.parse(str_fmt, ee.Date(first_im.get('system:time_start')).format(str_fmt)) + # Filter collection, sort by date, and get the first image's timestamp + first_im = ( + l5.filterBounds(aoi) + .filterDate(start_date, end_date) + .limit(1, 'system:time_start') # Explicitly limit by earliest date + .reduceColumns(ee.Reducer.first(), ['system:time_start']) + ) + + # Convert timestamp to formatted date + first_date = ee.Date(first_im.get('first')).format('YYYY-MM-dd') + + return first_date def run_process_long(res_name, res_polygon, start, end, datadir, results_per_iter=RESULTS_PER_ITER): fo = start #fo: first observation @@ -401,7 +410,7 @@ def run_process_long(res_name, res_polygon, start, end, datadir, results_per_ite if(number_of_images): # getting first observation in the filtered collection print('Checking first observation date in the given time interval.') - fo = get_first_obs(start, end).format('YYYY-MM-dd').getInfo() + fo = get_first_obs(start, end).getInfo() first_obs = datetime.strptime(fo, '%Y-%m-%d') print(f"First Observation: {first_obs}") diff --git a/src/rat/core/sarea/sarea_cli_l7.py b/src/rat/core/sarea/sarea_cli_l7.py index 0800b6c6..a4c0b881 100644 --- a/src/rat/core/sarea/sarea_cli_l7.py +++ b/src/rat/core/sarea/sarea_cli_l7.py @@ -417,9 +417,18 @@ def generate_timeseries(dates): return imcoll def get_first_obs(start_date, end_date): - first_im = l7.filterBounds(aoi).filterDate(start_date, end_date).first() - str_fmt = 'YYYY-MM-dd' - return ee.Date.parse(str_fmt, ee.Date(first_im.get('system:time_start')).format(str_fmt)) + # Filter collection, sort by date, and get the first image's timestamp + first_im = ( + l7.filterBounds(aoi) + .filterDate(start_date, end_date) + .limit(1, 'system:time_start') # Explicitly limit by earliest date + .reduceColumns(ee.Reducer.first(), ['system:time_start']) + ) + + # Convert timestamp to formatted date + first_date = ee.Date(first_im.get('first')).format('YYYY-MM-dd') + + return first_date def run_process_long(res_name, res_polygon, start, end, datadir, results_per_iter=RESULTS_PER_ITER): fo = start #fo: first observation @@ -435,7 +444,7 @@ def run_process_long(res_name, res_polygon, start, end, datadir, results_per_ite if(number_of_images): # getting first observation in the filtered collection print('Checking first observation date in the given time interval.') - fo = get_first_obs(start, end).format('YYYY-MM-dd').getInfo() + fo = get_first_obs(start, end).getInfo() first_obs = datetime.strptime(fo, '%Y-%m-%d') print(f"First Observation: {first_obs}") diff --git a/src/rat/core/sarea/sarea_cli_l8.py b/src/rat/core/sarea/sarea_cli_l8.py index 47d609f0..fb70029f 100644 --- a/src/rat/core/sarea/sarea_cli_l8.py +++ b/src/rat/core/sarea/sarea_cli_l8.py @@ -413,9 +413,18 @@ def generate_timeseries(dates): return imcoll def get_first_obs(start_date, end_date): - first_im = l8.filterBounds(aoi).filterDate(start_date, end_date).first() - str_fmt = 'YYYY-MM-dd' - return ee.Date.parse(str_fmt, ee.Date(first_im.get('system:time_start')).format(str_fmt)) + # Filter collection, sort by date, and get the first image's timestamp + first_im = ( + l8.filterBounds(aoi) + .filterDate(start_date, end_date) + .limit(1, 'system:time_start') # Explicitly limit by earliest date + .reduceColumns(ee.Reducer.first(), ['system:time_start']) + ) + + # Convert timestamp to formatted date + first_date = ee.Date(first_im.get('first')).format('YYYY-MM-dd') + + return first_date def run_process_long(res_name, res_polygon, start, end, datadir, results_per_iter=RESULTS_PER_ITER): fo = start @@ -431,7 +440,7 @@ def run_process_long(res_name, res_polygon, start, end, datadir, results_per_ite number_of_images = 1 # more than a month difference simply run, so no need to calculate number_of_images if(number_of_images): - fo = get_first_obs(start, end).format('YYYY-MM-dd').getInfo() + fo = get_first_obs(start, end).getInfo() first_obs = datetime.strptime(fo, '%Y-%m-%d') scratchdir = os.path.join(datadir, "_scratch") diff --git a/src/rat/core/sarea/sarea_cli_l9.py b/src/rat/core/sarea/sarea_cli_l9.py index e91b7ea7..beb808bc 100644 --- a/src/rat/core/sarea/sarea_cli_l9.py +++ b/src/rat/core/sarea/sarea_cli_l9.py @@ -415,9 +415,18 @@ def generate_timeseries(dates): return imcoll def get_first_obs(start_date, end_date): - first_im = l9.filterBounds(aoi).filterDate(start_date, end_date).first() - str_fmt = 'YYYY-MM-dd' - return ee.Date.parse(str_fmt, ee.Date(first_im.get('system:time_start')).format(str_fmt)) + # Filter collection, sort by date, and get the first image's timestamp + first_im = ( + l9.filterBounds(aoi) + .filterDate(start_date, end_date) + .limit(1, 'system:time_start') # Explicitly limit by earliest date + .reduceColumns(ee.Reducer.first(), ['system:time_start']) + ) + + # Convert timestamp to formatted date + first_date = ee.Date(first_im.get('first')).format('YYYY-MM-dd') + + return first_date def run_process_long(res_name, res_polygon, start, end, datadir, results_per_iter=RESULTS_PER_ITER): fo = start @@ -436,7 +445,7 @@ def run_process_long(res_name, res_polygon, start, end, datadir, results_per_ite number_of_images = 1 # more than a month difference simply run, so no need to calculate number_of_images if(number_of_images): - fo = get_first_obs(start, end).format('YYYY-MM-dd').getInfo() + fo = get_first_obs(start, end).getInfo() first_obs = datetime.strptime(fo, '%Y-%m-%d') scratchdir = os.path.join(datadir, "_scratch") diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 86a5701b..240416a4 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -464,9 +464,18 @@ def generate_timeseries(dates): return imcoll def get_first_obs(start_date, end_date): - first_im = s2.filterBounds(aoi).filterDate(start_date, end_date).first() - str_fmt = 'YYYY-MM-dd' - return ee.Date.parse(str_fmt, ee.Date(first_im.get('system:time_start')).format(str_fmt)) + # Filter collection, sort by date, and get the first image's timestamp + first_im = ( + s2.filterBounds(aoi) + .filterDate(start_date, end_date) + .limit(1, 'system:time_start') # Explicitly limit by earliest date + .reduceColumns(ee.Reducer.first(), ['system:time_start']) + ) + + # Convert timestamp to formatted date + first_date = ee.Date(first_im.get('first')).format('YYYY-MM-dd') + + return first_date def run_process_long(res_name,res_polygon, start, end, datadir, results_per_iter=RESULTS_PER_ITER): fo = start @@ -482,7 +491,7 @@ def run_process_long(res_name,res_polygon, start, end, datadir, results_per_iter number_of_images = 1 # more than a month difference simply run, so no need to calculate number_of_images if(number_of_images): - fo = get_first_obs(start, end).format('YYYY-MM-dd').getInfo() + fo = get_first_obs(start, end).getInfo() first_obs = datetime.strptime(fo, '%Y-%m-%d') scratchdir = os.path.join(datadir, "_scratch") From 9ec430e64f2596813f56eb1438e4f7b2b3baed78 Mon Sep 17 00:00:00 2001 From: Sanchit Date: Mon, 3 Feb 2025 02:47:49 +0000 Subject: [PATCH 064/102] Added error handling if no _scratch files are formed. --- src/rat/core/sarea/sarea_cli_l5.py | 2 +- src/rat/core/sarea/sarea_cli_l7.py | 2 +- src/rat/core/sarea/sarea_cli_l8.py | 15 +++++++++------ src/rat/core/sarea/sarea_cli_l9.py | 13 ++++++++----- src/rat/core/sarea/sarea_cli_s2.py | 11 +++++++---- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_l5.py b/src/rat/core/sarea/sarea_cli_l5.py index 00e36fbf..cff41afc 100644 --- a/src/rat/core/sarea/sarea_cli_l5.py +++ b/src/rat/core/sarea/sarea_cli_l5.py @@ -550,7 +550,7 @@ def run_process_long(res_name, res_polygon, start, end, datadir, results_per_ite return savepath else: - print("Observed data could not be processed to get surface area.") + print(f"Observed data between {start} and {end} could not be processed to get surface area. It may be due to cloud cover or other issues, Quitting!") return None else: print(f"No observation observed between {start} and {end}. Quitting!") diff --git a/src/rat/core/sarea/sarea_cli_l7.py b/src/rat/core/sarea/sarea_cli_l7.py index a4c0b881..8a466ef0 100644 --- a/src/rat/core/sarea/sarea_cli_l7.py +++ b/src/rat/core/sarea/sarea_cli_l7.py @@ -580,7 +580,7 @@ def run_process_long(res_name, res_polygon, start, end, datadir, results_per_ite return savepath else: - print("Observed data could not be processed to get surface area.") + print(f"Observed data between {start} and {end} could not be processed to get surface area. It may be due to cloud cover or other issues, Quitting!") return None else: print(f"No observation observed between {start} and {end}. Quitting!") diff --git a/src/rat/core/sarea/sarea_cli_l8.py b/src/rat/core/sarea/sarea_cli_l8.py index fb70029f..a4539cd4 100644 --- a/src/rat/core/sarea/sarea_cli_l8.py +++ b/src/rat/core/sarea/sarea_cli_l8.py @@ -573,14 +573,17 @@ def run_process_long(res_name, res_polygon, start, end, datadir, results_per_ite # Combine the files into one database to_combine.extend([os.path.join(savedir, f) for f in os.listdir(savedir) if f.endswith(".csv")]) + if len(to_combine): + files = [pd.read_csv(f, parse_dates=["mosaic_enddate"]).set_index("mosaic_enddate") for f in to_combine] + data = pd.concat(files).drop_duplicates().sort_values("mosaic_enddate") - files = [pd.read_csv(f, parse_dates=["mosaic_enddate"]).set_index("mosaic_enddate") for f in to_combine] - data = pd.concat(files).drop_duplicates().sort_values("mosaic_enddate") + data.to_csv(savepath) - data.to_csv(savepath) - - return savepath - + return savepath + else: + print(f"Observed data between {start} and {end} could not be processed to get surface area. It may be due to cloud cover or other issues, Quitting!") + return None + else: print(f"No observation observed between {start} and {end}. Quitting!") return None diff --git a/src/rat/core/sarea/sarea_cli_l9.py b/src/rat/core/sarea/sarea_cli_l9.py index beb808bc..e55aa77f 100644 --- a/src/rat/core/sarea/sarea_cli_l9.py +++ b/src/rat/core/sarea/sarea_cli_l9.py @@ -578,13 +578,16 @@ def run_process_long(res_name, res_polygon, start, end, datadir, results_per_ite # Combine the files into one database to_combine.extend([os.path.join(savedir, f) for f in os.listdir(savedir) if f.endswith(".csv")]) + if len(to_combine): + files = [pd.read_csv(f, parse_dates=["mosaic_enddate"]).set_index("mosaic_enddate") for f in to_combine] + data = pd.concat(files).drop_duplicates().sort_values("mosaic_enddate") - files = [pd.read_csv(f, parse_dates=["mosaic_enddate"]).set_index("mosaic_enddate") for f in to_combine] - data = pd.concat(files).drop_duplicates().sort_values("mosaic_enddate") + data.to_csv(savepath) - data.to_csv(savepath) - - return savepath + return savepath + else: + print(f"Observed data between {start} and {end} could not be processed to get surface area. It may be due to cloud cover or other issues, Quitting!") + return None else: print(f"No observation observed between {start} and {end}. Quitting!") diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 240416a4..52e0998b 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -665,11 +665,14 @@ def run_process_long(res_name,res_polygon, start, end, datadir, results_per_iter # Combine the files into one database to_combine.extend([os.path.join(savedir, f) for f in os.listdir(savedir) if f.endswith(".csv")]) + if len(to_combine): + files = [pd.read_csv(f, parse_dates=["date"]).set_index("date") for f in to_combine] + data = pd.concat(files).drop_duplicates().sort_values("date") - files = [pd.read_csv(f, parse_dates=["date"]).set_index("date") for f in to_combine] - data = pd.concat(files).drop_duplicates().sort_values("date") - - data.to_csv(savepath) + data.to_csv(savepath) + else: + print(f"Observed data between {start} and {end} could not be processed to get surface area. It may be due to cloud cover or other issues, Quitting!") + return None else: print(f"No observation observed between {start} and {end}. Quitting!") return None From 370887657eb656200fb964f658d9f1a7e4d6e4bd Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 2 Feb 2025 23:26:30 -0800 Subject: [PATCH 065/102] Added error handling for computation timed out due to small spatial scale for sentinel 2 --- src/rat/core/sarea/sarea_cli_s2.py | 85 +++++++++++++++++++---------- src/rat/core/sarea/sarea_cli_sar.py | 2 +- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 52e0998b..ca0b980d 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -36,7 +36,9 @@ def grouper(iterable, n, *, incomplete='fill', fillvalue=None): rgb_vis_params = {"bands":["B4","B3","B2"],"min":0,"max":0.4} NDWI_THRESHOLD = 0.3; -SMALL_SCALE = 20; +SPATIAL_SCALE_SMALL = 20; +SPATIAL_SCALE_MEDIUM = 30; +SPATIAL_SCALE_LARGE = 50; MEDIUM_SCALE = 120; LARGE_SCALE = 500; BUFFER_DIST = 500 @@ -73,7 +75,7 @@ def scl_cloud_mask(im): # cloud_area = cloudmask.reduceRegion( # reducer = ee.Reducer.sum(), # geometry = aoi, - # scale = SMALL_SCALE, + # scale = SPATIAL_SCALE_SMALL, # maxPixels = 1e10 # ).get('') im = im.addBands(cloudmask) @@ -122,14 +124,14 @@ def calc_avg_mbwi(cluster_val): return water_cluster -def clustering(im): +def clustering(im, spatial_scale): ## Agglomerative Clustering isn't available, using Cascade K-Means Clustering based on ## calinski harabasz's work ## https:##developers.google.com/earth-engine/apidocs/ee-clusterer-wekacascadekmeans band_subset = ee.List(['NDWI', 'B12']) sampled_pts = im.select(band_subset).sample( region = aoi, - scale = SMALL_SCALE, + scale = SPATIAL_SCALE_SMALL, numPixels = 4999 ## limit of 5k points, staying at 4k ) no_sampled_pts = sampled_pts.size() @@ -189,7 +191,7 @@ def if_clustering_not_possible(im): return im -def process_image(im): +def process_image(im, spatial_scale): # Process Image ndwi = im.normalizedDifference(['B3', 'B8']).rename('NDWI'); @@ -211,7 +213,7 @@ def process_image(im): cloud_area = aoi.area().subtract(im.select('cloud').Not().multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, - scale = SMALL_SCALE, + scale = spatial_scale, maxPixels = 1e10 ).get('cloud')) cloud_percent = cloud_area.multiply(100).divide(aoi.area()) @@ -224,7 +226,7 @@ def process_image(im): ee.Image( ee.Algorithms.If( CLOUD_LIMIT_SATISFIED, - clustering(im), + clustering(im, spatial_scale), ee.Image.constant(-1e6) ) ) @@ -238,7 +240,7 @@ def process_image(im): ee.Number(im.select('water_map_clustering').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, - scale = SMALL_SCALE, + scale = spatial_scale, maxPixels = 1e10 ).get('water_map_clustering')), ee.Number(-1e6) @@ -250,7 +252,7 @@ def process_image(im): ee.Number(im.select('water_map_clustering').neq(1).multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, - scale = SMALL_SCALE, + scale = spatial_scale, maxPixels = 1e10 ).get('water_map_clustering')), ee.Number(-1e6) @@ -263,7 +265,7 @@ def process_image(im): ee.Number(im.select('water_map_clustering').eq(1).multiply(im.select(RED_BAND_NAME)).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, - scale = SMALL_SCALE, + scale = spatial_scale, maxPixels = 1e10 ).get('water_map_clustering')), ee.Number(-1e6) @@ -276,7 +278,7 @@ def process_image(im): ee.Number(im.select('water_map_clustering').eq(1).multiply(im.select(GREEN_BAND_NAME)).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, - scale = SMALL_SCALE, + scale = spatial_scale, maxPixels = 1e10 ).get('water_map_clustering')), ee.Number(-1e6) @@ -289,7 +291,7 @@ def process_image(im): ee.Number(im.select('water_map_clustering').eq(1).multiply(im.select(NIR_BAND_NAME)).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, - scale = SMALL_SCALE, + scale = spatial_scale, maxPixels = 1e10 ).get('water_map_clustering')), ee.Number(-1e6) @@ -303,7 +305,7 @@ def process_image(im): GREEN_BAND_NAME)).reduceRegion( reducer = ee.Reducer.mean(), geometry = aoi, - scale = SMALL_SCALE, + scale = spatial_scale, maxPixels = 1e10 ).get('water_map_clustering')), ee.Number(-1e6) @@ -317,7 +319,7 @@ def process_image(im): RED_BAND_NAME)).reduceRegion( reducer = ee.Reducer.mean(), geometry = aoi, - scale = SMALL_SCALE, + scale = spatial_scale, maxPixels = 1e10 ).get('water_map_clustering')), ee.Number(-1e6) @@ -337,13 +339,13 @@ def process_image(im): return im -def postprocess(im, bandName='water_map_clustering'): +def postprocess(im, spatial_scale, bandName='water_map_clustering'): gswd_masked = gswd.updateMask(im.select(bandName).eq(1)) hist = ee.List(gswd_masked.reduceRegion( reducer = ee.Reducer.autoHistogram(minBucketWidth = 1), geometry = aoi, - scale = SMALL_SCALE, + scale = spatial_scale, maxPixels = 1e10 ).get('occurrence')) @@ -365,7 +367,7 @@ def if_hist_not_null(im,hist): corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, - scale = SMALL_SCALE, + scale = spatial_scale, maxPixels = 1e10 ).get('water_map_zhao_gao')) @@ -387,7 +389,7 @@ def if_hist_null(im): )) return improved -def postprocess_wrapper(im, bandName='water_map_clustering'): +def postprocess_wrapper(im, spatial_scale, bandName='water_map_clustering'): def do_not_postprocess(): default_im = ee.Image.constant(-1).rename(bandName) @@ -404,7 +406,7 @@ def do_not_postprocess(): improved = ee.Image(ee.Algorithms.If( processing_successful, - postprocess(im, bandName), + postprocess(im, spatial_scale, bandName), do_not_postprocess() )) @@ -417,7 +419,7 @@ def do_not_postprocess(): def calc_ndwi(im): return im.addBands(im.normalizedDifference(['B3', 'B8']).rename('NDWI')) -def process_date(date): +def process_date(date, spatial_scale): date = ee.Date(date) to_date = date.advance(1, 'day') from_date = date.advance(-(TEMPORAL_RESOLUTION-1), 'day') @@ -441,7 +443,7 @@ def not_enough_images(): im = ee.Image( ee.Algorithms.If( ENOUGH_IMAGES, - process_image(im), + process_image(im, spatial_scale), not_enough_images() ) ) @@ -455,9 +457,9 @@ def not_enough_images(): return ee.Image(im) -def generate_timeseries(dates): +def generate_timeseries(dates, spatial_scale): # raw_ts = process_date(dates.get(4)) - raw_ts = dates.map(process_date) + raw_ts = dates.map(lambda date: process_date(date,spatial_scale)) # raw_ts = raw_ts.removeAll([0]); imcoll = ee.ImageCollection.fromImages(raw_ts) @@ -542,17 +544,44 @@ def run_process_long(res_name,res_polygon, start, end, datadir, results_per_iter while results_per_iter >= MIN_RESULTS_PER_ITER: # try to run for each subset of dates try: + scale_to_use = SPATIAL_SCALE_SMALL for subset_dates in grouped_dates: # try to run for subset_dates with results_per_iter try: print(subset_dates) dates = ee.List([ee.Date(d) for d in subset_dates if d is not None]) - ts_imcoll = generate_timeseries(dates) - postprocessed_ts_imcoll = ts_imcoll.map(postprocess_wrapper) - # Download the data locally - ts_imcoll_L = ts_imcoll.getInfo() - postprocessed_ts_imcoll_L = postprocessed_ts_imcoll.getInfo() + success_status = 0 # initialize success_status with 0 which will remain 0 on fail attempt and become 1 on successful attempt + while (success_status == 0): + try: + ts_imcoll = generate_timeseries(dates, spatial_scale=scale_to_use) + # Postprocess the image collection + postprocessed_ts_imcoll = ts_imcoll.map(lambda img: postprocess_wrapper(img, spatial_scale=scale_to_use)) + # Run the generate timeseries + ts_imcoll_L = ts_imcoll.getInfo() + # Run thw postprocess of image collection + postprocessed_ts_imcoll_L = postprocessed_ts_imcoll.getInfo() + except Exception as e: + log.error(e) + # Adjust scale_to_use only if error includes "Computation timed out" + if "Computation timed out" in str(e): + if scale_to_use == SPATIAL_SCALE_SMALL: + scale_to_use = SPATIAL_SCALE_MEDIUM + print(f"Trying with larger spatial resolution: {scale_to_use} m.") + success_status = 0 + continue + elif scale_to_use == SPATIAL_SCALE_MEDIUM: + scale_to_use = SPATIAL_SCALE_LARGE + print(f"Trying with larger spatial resolution: {scale_to_use} m.") + success_status = 0 + continue + else: + print("Trying with larger spatial resolution failed. Moving to next iteration.") + scale_to_use = SPATIAL_SCALE_MEDIUM + success_status = 1 + break + else: + success_status = 1 # Parse the data to create dataframe PROCESSING_STATUSES = [] POSTPROCESSING_STATUSES = [] diff --git a/src/rat/core/sarea/sarea_cli_sar.py b/src/rat/core/sarea/sarea_cli_sar.py index 90f080bf..23f6db51 100644 --- a/src/rat/core/sarea/sarea_cli_sar.py +++ b/src/rat/core/sarea/sarea_cli_sar.py @@ -153,7 +153,7 @@ def retrieve_sar(start_date, end_date, res='ys'): #ys-year-start frequency dfs.append(ee_get_data(begin, end, spatial_scale=scale_to_use)) except Exception as e: log.error(e) - # Adjust results_per_iter only if error includes "Too many concurrent aggregations" + # Adjust scale_to_use only if error includes "Computation timed out" if "Computation timed out" in str(e): if scale_to_use == SPATIAL_SCALE_SMALL: scale_to_use = SPATIAL_SCALE_MEDIUM From 1169fe8ac8b45291b6455e187b46bc04289807d3 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 3 Feb 2025 00:02:42 -0800 Subject: [PATCH 066/102] adjusted the medium and large scale to further resolve error for large reservoirs --- src/rat/core/sarea/sarea_cli_s2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index ca0b980d..45d38125 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -37,8 +37,8 @@ def grouper(iterable, n, *, incomplete='fill', fillvalue=None): NDWI_THRESHOLD = 0.3; SPATIAL_SCALE_SMALL = 20; -SPATIAL_SCALE_MEDIUM = 30; -SPATIAL_SCALE_LARGE = 50; +SPATIAL_SCALE_MEDIUM = 50; +SPATIAL_SCALE_LARGE = 200; MEDIUM_SCALE = 120; LARGE_SCALE = 500; BUFFER_DIST = 500 From e5bc116fa5112754ca68c331be5fcb31b6002f33 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 3 Feb 2025 00:10:33 -0800 Subject: [PATCH 067/102] Using success_status to further process in case of errors --- src/rat/core/sarea/sarea_cli_s2.py | 167 +++++++++++++++-------------- 1 file changed, 85 insertions(+), 82 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 45d38125..48a01410 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -578,91 +578,94 @@ def run_process_long(res_name,res_polygon, start, end, datadir, results_per_iter else: print("Trying with larger spatial resolution failed. Moving to next iteration.") scale_to_use = SPATIAL_SCALE_MEDIUM - success_status = 1 + success_status = -1 break else: success_status = 1 - # Parse the data to create dataframe - PROCESSING_STATUSES = [] - POSTPROCESSING_STATUSES = [] - cloud_areas = [] - cloud_percents = [] - from_dates = [] - to_dates = [] - obs_dates = [] - non_water_areas = [] - water_areas = [] - water_areas_zhaogao = [] - water_red_sums = [] - water_green_sums = [] - water_nir_sums = [] - water_red_green_means = [] - water_nir_red_means = [] - for f, f_postprocessed in zip(ts_imcoll_L['features'], postprocessed_ts_imcoll_L['features']): - PROCESSING_STATUS = f['properties']['PROCESSING_SUCCESSFUL'] - PROCESSING_STATUSES.append(PROCESSING_STATUS) - POSTPROCESSING_STATUS = f_postprocessed['properties']['POSTPROCESSING_SUCCESSFUL'] - POSTPROCESSING_STATUSES.append(POSTPROCESSING_STATUS) - obs_dates.append(pd.to_datetime(f['properties']['system:time_start'])) - from_dates.append(pd.to_datetime(f['properties']['from_date'])) - to_dates.append(pd.to_datetime(f['properties']['to_date'])) - if PROCESSING_STATUS: - water_areas.append(f['properties']['water_area_clustering']) - non_water_areas.append(f['properties']['non_water_area_clustering']) - cloud_areas.append(f['properties']['cloud_area']) - cloud_percents.append(f['properties']['cloud_percent']) - water_red_sums.append(f['properties']['water_red_sum']) - water_green_sums.append(f['properties']['water_green_sum']) - water_nir_sums.append(f['properties']['water_nir_sum']) - water_red_green_means.append(f['properties']['water_red_green_mean']) - water_nir_red_means.append(f['properties']['water_nir_red_mean']) - else: - water_areas.append(np.nan) - non_water_areas.append(np.nan) - cloud_areas.append(np.nan) - cloud_percents.append(np.nan) - water_red_sums.append(np.nan) - water_green_sums.append(np.nan) - water_nir_sums.append(np.nan) - water_red_green_means.append(np.nan) - water_nir_red_means.append(np.nan) - if POSTPROCESSING_STATUS: - water_areas_zhaogao.append(f_postprocessed['properties']['corrected_area']) - else: - water_areas_zhaogao.append(np.nan) - - df = pd.DataFrame({ - 'date': obs_dates, - 'PROCESSING_STATUS': PROCESSING_STATUSES, - 'POSTPROCESSING_STATUS': POSTPROCESSING_STATUSES, - 'from_date': from_dates, - 'to_date': to_dates, - 'cloud_area': cloud_areas, - 'cloud_percent': cloud_percents, - 'water_area_uncorrected': water_areas, - 'non_water_area': non_water_areas, - 'water_area_corrected': water_areas_zhaogao, - 'water_red_sum': water_red_sums, - 'water_green_sum': water_green_sums, - 'water_nir_sum': water_nir_sums, - 'water_red_green_mean': water_red_green_means, - 'water_nir_red_mean': water_nir_red_means - }).set_index('date') - - fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") - df.to_csv(fname) - print(df.tail()) - - s_time = randint(5, 10) - print(f"Sleeping for {s_time} seconds") - time.sleep(s_time) - - if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: - print(f"Quitting: Reached enddate {enddate}") - break - elif df.index[-1].strftime('%Y-%m-%d') == fo: - print(f"Reached last available observation - {fo}") - break + if success_status==1: + # Parse the data to create dataframe + PROCESSING_STATUSES = [] + POSTPROCESSING_STATUSES = [] + cloud_areas = [] + cloud_percents = [] + from_dates = [] + to_dates = [] + obs_dates = [] + non_water_areas = [] + water_areas = [] + water_areas_zhaogao = [] + water_red_sums = [] + water_green_sums = [] + water_nir_sums = [] + water_red_green_means = [] + water_nir_red_means = [] + for f, f_postprocessed in zip(ts_imcoll_L['features'], postprocessed_ts_imcoll_L['features']): + PROCESSING_STATUS = f['properties']['PROCESSING_SUCCESSFUL'] + PROCESSING_STATUSES.append(PROCESSING_STATUS) + POSTPROCESSING_STATUS = f_postprocessed['properties']['POSTPROCESSING_SUCCESSFUL'] + POSTPROCESSING_STATUSES.append(POSTPROCESSING_STATUS) + obs_dates.append(pd.to_datetime(f['properties']['system:time_start'])) + from_dates.append(pd.to_datetime(f['properties']['from_date'])) + to_dates.append(pd.to_datetime(f['properties']['to_date'])) + if PROCESSING_STATUS: + water_areas.append(f['properties']['water_area_clustering']) + non_water_areas.append(f['properties']['non_water_area_clustering']) + cloud_areas.append(f['properties']['cloud_area']) + cloud_percents.append(f['properties']['cloud_percent']) + water_red_sums.append(f['properties']['water_red_sum']) + water_green_sums.append(f['properties']['water_green_sum']) + water_nir_sums.append(f['properties']['water_nir_sum']) + water_red_green_means.append(f['properties']['water_red_green_mean']) + water_nir_red_means.append(f['properties']['water_nir_red_mean']) + else: + water_areas.append(np.nan) + non_water_areas.append(np.nan) + cloud_areas.append(np.nan) + cloud_percents.append(np.nan) + water_red_sums.append(np.nan) + water_green_sums.append(np.nan) + water_nir_sums.append(np.nan) + water_red_green_means.append(np.nan) + water_nir_red_means.append(np.nan) + if POSTPROCESSING_STATUS: + water_areas_zhaogao.append(f_postprocessed['properties']['corrected_area']) + else: + water_areas_zhaogao.append(np.nan) + + df = pd.DataFrame({ + 'date': obs_dates, + 'PROCESSING_STATUS': PROCESSING_STATUSES, + 'POSTPROCESSING_STATUS': POSTPROCESSING_STATUSES, + 'from_date': from_dates, + 'to_date': to_dates, + 'cloud_area': cloud_areas, + 'cloud_percent': cloud_percents, + 'water_area_uncorrected': water_areas, + 'non_water_area': non_water_areas, + 'water_area_corrected': water_areas_zhaogao, + 'water_red_sum': water_red_sums, + 'water_green_sum': water_green_sums, + 'water_nir_sum': water_nir_sums, + 'water_red_green_mean': water_red_green_means, + 'water_nir_red_mean': water_nir_red_means + }).set_index('date') + + fname = os.path.join(savedir, f"{df.index[0].strftime('%Y%m%d')}_{df.index[-1].strftime('%Y%m%d')}_{res_name}.csv") + df.to_csv(fname) + print(df.tail()) + + s_time = randint(5, 10) + print(f"Sleeping for {s_time} seconds") + time.sleep(s_time) + + if (datetime.strptime(enddate, "%Y-%m-%d")-df.index[-1]).days < TEMPORAL_RESOLUTION: + print(f"Quitting: Reached enddate {enddate}") + break + elif df.index[-1].strftime('%Y-%m-%d') == fo: + print(f"Reached last available observation - {fo}") + break + else: + raise Exception("Skipping this iteration of dates due to failed attempt(s).") # If exception is "Too many concurrent aggregations", reduce results_per_iter # and rerun for loop for leftover dates by raising Exception. # Else just print the exception and continue. From 619297304037f843cfdf39700b9f537b5133e2f1 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 3 Feb 2025 00:14:23 -0800 Subject: [PATCH 068/102] Corrected labeling of warnings and error for changing scale in case of computation timed out error --- src/rat/core/sarea/sarea_cli_s2.py | 6 +++--- src/rat/core/sarea/sarea_cli_sar.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 48a01410..52cbacd5 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -567,16 +567,16 @@ def run_process_long(res_name,res_polygon, start, end, datadir, results_per_iter if "Computation timed out" in str(e): if scale_to_use == SPATIAL_SCALE_SMALL: scale_to_use = SPATIAL_SCALE_MEDIUM - print(f"Trying with larger spatial resolution: {scale_to_use} m.") + log.warning(f"Trying with larger spatial resolution: {scale_to_use} m.") success_status = 0 continue elif scale_to_use == SPATIAL_SCALE_MEDIUM: scale_to_use = SPATIAL_SCALE_LARGE - print(f"Trying with larger spatial resolution: {scale_to_use} m.") + log.warning(f"Trying with larger spatial resolution: {scale_to_use} m.") success_status = 0 continue else: - print("Trying with larger spatial resolution failed. Moving to next iteration.") + log.error("Trying with larger spatial resolution failed. Moving to next iteration.") scale_to_use = SPATIAL_SCALE_MEDIUM success_status = -1 break diff --git a/src/rat/core/sarea/sarea_cli_sar.py b/src/rat/core/sarea/sarea_cli_sar.py index 23f6db51..bf70ec8a 100644 --- a/src/rat/core/sarea/sarea_cli_sar.py +++ b/src/rat/core/sarea/sarea_cli_sar.py @@ -157,16 +157,16 @@ def retrieve_sar(start_date, end_date, res='ys'): #ys-year-start frequency if "Computation timed out" in str(e): if scale_to_use == SPATIAL_SCALE_SMALL: scale_to_use = SPATIAL_SCALE_MEDIUM - print(f"Trying with larger spatial resolution: {scale_to_use} m.") + log.warning(f"Trying with larger spatial resolution: {scale_to_use} m.") success_status = 0 continue elif scale_to_use == SPATIAL_SCALE_MEDIUM: scale_to_use = SPATIAL_SCALE_LARGE - print(f"Trying with larger spatial resolution: {scale_to_use} m.") + log.warning(f"Trying with larger spatial resolution: {scale_to_use} m.") success_status = 0 continue else: - print("Trying with larger spatial resolution failed. Moving to next iteration.") + log.error("Trying with larger spatial resolution failed. Moving to next iteration.") scale_to_use = SPATIAL_SCALE_MEDIUM success_status = 1 break From c9c5a4f88f412b662adadd1004b7068205d03d69 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 3 Feb 2025 01:06:00 -0800 Subject: [PATCH 069/102] corrected area calculations for different spatial scales in s2 --- src/rat/core/sarea/sarea_cli_s2.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 52cbacd5..951f3809 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -210,12 +210,13 @@ def process_image(im, spatial_scale): # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) - cloud_area = aoi.area().subtract(im.select('cloud').Not().multiply(ee.Image.pixelArea()).reduceRegion( + cloud_pixel_number = aoi.area().subtract(im.select('cloud').Not().reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, scale = spatial_scale, maxPixels = 1e10 ).get('cloud')) + cloud_area = ee.Number(cloud_pixel_number).multiply(spatial_scale).multiply(spatial_scale) cloud_percent = cloud_area.multiply(100).divide(aoi.area()) CLOUD_LIMIT_SATISFIED = cloud_percent.lt(CLOUD_COVER_LIMIT) @@ -237,24 +238,24 @@ def process_image(im, spatial_scale): water_area_clustering = ee.Number( ee.Algorithms.If( CLOUD_LIMIT_SATISFIED, - ee.Number(im.select('water_map_clustering').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( + ee.Number(im.select('water_map_clustering').eq(1).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('water_map_clustering')), + ).get('water_map_clustering').multiply(spatial_scale**2)), ee.Number(-1e6) ) ) non_water_area_clustering = ee.Number( ee.Algorithms.If( CLOUD_LIMIT_SATISFIED, - ee.Number(im.select('water_map_clustering').neq(1).multiply(ee.Image.pixelArea()).reduceRegion( + ee.Number(im.select('water_map_clustering').neq(1).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('water_map_clustering')), + ).get('water_map_clustering').multiply(spatial_scale**2)), ee.Number(-1e6) ) ) @@ -364,12 +365,12 @@ def if_hist_not_null(im,hist): improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) - corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( + corrected_area = ee.Number(improved.select('water_map_zhao_gao').reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('water_map_zhao_gao')) + ).get('water_map_zhao_gao').multiply(spatial_scale**2)) improved = improved.set("corrected_area", corrected_area.multiply(1e-6)); improved = improved.set("POSTPROCESSING_SUCCESSFUL", 1); From d07a087f927e31ab8c465794eea5b3442ac8c2f8 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 3 Feb 2025 01:20:10 -0800 Subject: [PATCH 070/102] Syntax error fix: ee.Number should multiply --- src/rat/core/sarea/sarea_cli_s2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 951f3809..23be8a63 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -243,7 +243,7 @@ def process_image(im, spatial_scale): geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('water_map_clustering').multiply(spatial_scale**2)), + ).get('water_map_clustering')).multiply(spatial_scale**2), ee.Number(-1e6) ) ) @@ -255,7 +255,7 @@ def process_image(im, spatial_scale): geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('water_map_clustering').multiply(spatial_scale**2)), + ).get('water_map_clustering')).multiply(spatial_scale**2), ee.Number(-1e6) ) ) @@ -370,7 +370,7 @@ def if_hist_not_null(im,hist): geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('water_map_zhao_gao').multiply(spatial_scale**2)) + ).get('water_map_zhao_gao')).multiply(spatial_scale**2) improved = improved.set("corrected_area", corrected_area.multiply(1e-6)); improved = improved.set("POSTPROCESSING_SUCCESSFUL", 1); From 9e14b7e9395026ecb85e03a93e3b33cf1e852e6c Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 3 Feb 2025 11:16:29 -0800 Subject: [PATCH 071/102] corrected formula of cloud area and cloud percent due to calculation of pixels first and then area --- src/rat/core/sarea/sarea_cli_s2.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 23be8a63..953472f0 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -210,13 +210,17 @@ def process_image(im, spatial_scale): # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) - cloud_pixel_number = aoi.area().subtract(im.select('cloud').Not().reduceRegion( + cloud_free_pixels = im.select('cloud').Not().reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('cloud')) - cloud_area = ee.Number(cloud_pixel_number).multiply(spatial_scale).multiply(spatial_scale) + ).get('cloud') + # Convert pixel count to area + cloud_free_area = ee.Number(cloud_free_pixels).multiply(spatial_scale**2) + + # Compute cloud-covered area + cloud_area = aoi.area().subtract(cloud_free_area) cloud_percent = cloud_area.multiply(100).divide(aoi.area()) CLOUD_LIMIT_SATISFIED = cloud_percent.lt(CLOUD_COVER_LIMIT) From 964d35fe82ab16fcd9bc53bffc72aa4dacc1a2d2 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 3 Feb 2025 14:06:21 -0800 Subject: [PATCH 072/102] Revert "Syntax error fix: ee.Number should multiply" This reverts commit c8758c64de190482dc74b45e7f8f2c832848e339. --- src/rat/core/sarea/sarea_cli_s2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 953472f0..2c5b0001 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -247,7 +247,7 @@ def process_image(im, spatial_scale): geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('water_map_clustering')).multiply(spatial_scale**2), + ).get('water_map_clustering').multiply(spatial_scale**2)), ee.Number(-1e6) ) ) @@ -259,7 +259,7 @@ def process_image(im, spatial_scale): geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('water_map_clustering')).multiply(spatial_scale**2), + ).get('water_map_clustering').multiply(spatial_scale**2)), ee.Number(-1e6) ) ) @@ -374,7 +374,7 @@ def if_hist_not_null(im,hist): geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('water_map_zhao_gao')).multiply(spatial_scale**2) + ).get('water_map_zhao_gao').multiply(spatial_scale**2)) improved = improved.set("corrected_area", corrected_area.multiply(1e-6)); improved = improved.set("POSTPROCESSING_SUCCESSFUL", 1); From 32570683c267d88f2ce5af807f2ec245b9bb0348 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 3 Feb 2025 14:07:39 -0800 Subject: [PATCH 073/102] Revert "corrected formula of cloud area and cloud percent due to calculation of pixels first and then area" This reverts commit e42e126a0c0c1484c5816b409d3cd4126756fa73. --- src/rat/core/sarea/sarea_cli_s2.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 2c5b0001..951f3809 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -210,17 +210,13 @@ def process_image(im, spatial_scale): # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) - cloud_free_pixels = im.select('cloud').Not().reduceRegion( + cloud_pixel_number = aoi.area().subtract(im.select('cloud').Not().reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('cloud') - # Convert pixel count to area - cloud_free_area = ee.Number(cloud_free_pixels).multiply(spatial_scale**2) - - # Compute cloud-covered area - cloud_area = aoi.area().subtract(cloud_free_area) + ).get('cloud')) + cloud_area = ee.Number(cloud_pixel_number).multiply(spatial_scale).multiply(spatial_scale) cloud_percent = cloud_area.multiply(100).divide(aoi.area()) CLOUD_LIMIT_SATISFIED = cloud_percent.lt(CLOUD_COVER_LIMIT) From 140c18e5ee0606e43214b05d5d7f75cef8a74fde Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Mon, 3 Feb 2025 14:13:04 -0800 Subject: [PATCH 074/102] Revert "corrected area calculations for different spatial scales in s2" This reverts commit 4293c63d8341d74479ffff9da152d433268c3b73. --- src/rat/core/sarea/sarea_cli_s2.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 951f3809..52cbacd5 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -210,13 +210,12 @@ def process_image(im, spatial_scale): # NDWI based water map: Classify water wherever NDWI is greater than NDWI_THRESHOLD and add water_map_NDWI band. im = im.addBands(ndwi.gte(NDWI_THRESHOLD).select(['NDWI'], ['water_map_NDWI'])) - cloud_pixel_number = aoi.area().subtract(im.select('cloud').Not().reduceRegion( + cloud_area = aoi.area().subtract(im.select('cloud').Not().multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, scale = spatial_scale, maxPixels = 1e10 ).get('cloud')) - cloud_area = ee.Number(cloud_pixel_number).multiply(spatial_scale).multiply(spatial_scale) cloud_percent = cloud_area.multiply(100).divide(aoi.area()) CLOUD_LIMIT_SATISFIED = cloud_percent.lt(CLOUD_COVER_LIMIT) @@ -238,24 +237,24 @@ def process_image(im, spatial_scale): water_area_clustering = ee.Number( ee.Algorithms.If( CLOUD_LIMIT_SATISFIED, - ee.Number(im.select('water_map_clustering').eq(1).reduceRegion( + ee.Number(im.select('water_map_clustering').eq(1).multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('water_map_clustering').multiply(spatial_scale**2)), + ).get('water_map_clustering')), ee.Number(-1e6) ) ) non_water_area_clustering = ee.Number( ee.Algorithms.If( CLOUD_LIMIT_SATISFIED, - ee.Number(im.select('water_map_clustering').neq(1).reduceRegion( + ee.Number(im.select('water_map_clustering').neq(1).multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('water_map_clustering').multiply(spatial_scale**2)), + ).get('water_map_clustering')), ee.Number(-1e6) ) ) @@ -365,12 +364,12 @@ def if_hist_not_null(im,hist): improved = ee.ImageCollection([water_map, gswd_improvement]).mosaic().select(['water_map'], ['water_map_zhao_gao']) - corrected_area = ee.Number(improved.select('water_map_zhao_gao').reduceRegion( + corrected_area = ee.Number(improved.select('water_map_zhao_gao').multiply(ee.Image.pixelArea()).reduceRegion( reducer = ee.Reducer.sum(), geometry = aoi, scale = spatial_scale, maxPixels = 1e10 - ).get('water_map_zhao_gao').multiply(spatial_scale**2)) + ).get('water_map_zhao_gao')) improved = improved.set("corrected_area", corrected_area.multiply(1e-6)); improved = improved.set("POSTPROCESSING_SUCCESSFUL", 1); From 3c8f852d3a5e518c67182d4e8d501672417a5f92 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Tue, 4 Feb 2025 04:49:10 -0800 Subject: [PATCH 075/102] Bug Fix: Sometimes band order was inconsistent leading to error in creating mosaic (when more than 1 image is available for AOI for 1 temporal resolution) --- src/rat/core/sarea/sarea_cli_s2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index 52cbacd5..a3ee2b22 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -417,7 +417,10 @@ def do_not_postprocess(): ## Code from here takes care of the time-series generation ## ############################################################/ def calc_ndwi(im): - return im.addBands(im.normalizedDifference(['B3', 'B8']).rename('NDWI')) + im = im.addBands(im.normalizedDifference(['B3', 'B8']).rename('NDWI')) + # Sort the bands in ascending order of their name for consistency + im = im.select(im.bandNames().sort()) + return im def process_date(date, spatial_scale): date = ee.Date(date) From 0bded09c83f0f4f52fb7fdf8d6e7c439a9533a16 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Wed, 5 Feb 2025 22:27:49 -0800 Subject: [PATCH 076/102] added coide to simplify geometry of complex shapes for surface area extraction step 10 to fix Computation timed out error --- src/rat/core/run_sarea.py | 9 ++++- src/rat/ee_utils/ee_utils.py | 75 +++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/rat/core/run_sarea.py b/src/rat/core/run_sarea.py index 0f4cfccc..3d06ef2e 100644 --- a/src/rat/core/run_sarea.py +++ b/src/rat/core/run_sarea.py @@ -3,6 +3,7 @@ from logging import getLogger from rat.utils.logging import LOG_NAME, NOTIFICATION, LOG_LEVEL1_NAME +from rat.ee_utils.ee_utils import simplify_geometry from rat.core.sarea.sarea_cli_s2 import sarea_s2 from rat.core.sarea.sarea_cli_l5 import sarea_l5 @@ -66,8 +67,12 @@ def run_sarea(start_date, end_date, sarea_save_dir, reservoirs_shpfile, shpfile_ else: bot_filter(sarea_save_dir,shpfile_column_dict,reservoirs_shpfile,**filt_options) -def run_sarea_for_res(reservoir_name, reservoir_area, reservoir_polygon, start_date, end_date, sarea_save_dir, nssc_save_dir): - +def run_sarea_for_res(reservoir_name, reservoir_area, reservoir_polygon, start_date, end_date, sarea_save_dir, nssc_save_dir, simplication=True): + + if simplication: + # Below function simplifies geometry with shape index (complexity) higher than a threshold, otherwise original geometry is retained + reservoir_polygon = simplify_geometry(reservoir_polygon) + # Obtain surface areas # Sentinel-2 log.debug(f"Reservoir: {reservoir_name}; Downloading Sentinel-2 data from {start_date} to {end_date}") diff --git a/src/rat/ee_utils/ee_utils.py b/src/rat/ee_utils/ee_utils.py index c29e0a6d..e4b345f8 100644 --- a/src/rat/ee_utils/ee_utils.py +++ b/src/rat/ee_utils/ee_utils.py @@ -20,4 +20,77 @@ def poly2feature(polygon,buffer_distance): g=ee.Geometry.Polygon(cords).buffer(buffer_distance) #buffer for polygon_object in meters feature = ee.Feature(g) - return feature \ No newline at end of file + return feature + +def shape_index(polygon): + """ + Calculate the shape index of a given polygon. + + The shape index is defined as the square of the perimeter divided by the area: + SI = (perimeter^2) / area + + Parameters: + polygon (shapely.geometry.Polygon): The input polygon. + + Returns: + float: The shape index value. + """ + return (polygon.length ** 2) / polygon.area + +def compute_initial_tolerance(si): + """ + Compute the initial simplification tolerance based on the shape index magnitude. + + The tolerance is set as 10^(-order_of_magnitude), where order_of_magnitude is + the exponent of the shape index rounded up. + + Parameters: + si (float): The shape index value. + + Returns: + float: The initial simplification tolerance. + """ + order_of_magnitude = math.ceil(math.log10(si)) # Get the exponent of SI + return 10 ** (-order_of_magnitude) # Set tolerance as inverse of order + +def simplify_geometry(polygon, threshold=800, initial_tolerance=None): + """ + Simplify a polygon iteratively until its shape index falls below a given threshold. + + If no initial tolerance is provided, it is determined dynamically using the + compute_initial_tolerance function. The simplification process gradually + increases the tolerance by a factor of 3.5 in each iteration. + + Parameters: + polygon (shapely.geometry.Polygon): The input polygon to simplify. + threshold (float, optional): The shape index threshold for stopping simplification. Default is 800. + initial_tolerance (float, optional): The starting tolerance for simplification. If None, + it is computed based on the shape index. + + Returns: + shapely.geometry.Polygon: The simplified polygon. + """ + simplification = 0 + si_original = shape_index(polygon) + si = si_original + if initial_tolerance is None: + initial_tolerance = compute_initial_tolerance(si) + else: + initial_tolerance = initial_tolerance + + tolerance = initial_tolerance # Start with an initial tolerance + while si > threshold: # Keep simplifying until shape index is below threshold + simplification = 1 + simplified_polygon = polygon.simplify(tolerance, preserve_topology=True) + + # Stop if no significant simplification occurs + if simplified_polygon == polygon: + break + + polygon = simplified_polygon + si = shape_index(polygon) + tolerance *= 3.5 # Gradually increase tolerance for more simplification + if simplification: + print(f"Using simplified geometry to extract surface area because of complex shape.") + print(f"Shape index of original geometry is {si_original} which is above threshold of {threshold}. Shape index of simplified geometry is {si}.") + return polygon \ No newline at end of file From d3f8ac2a2281e6ac9e8164756742d2b738c60ecf Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Wed, 5 Feb 2025 22:35:11 -0800 Subject: [PATCH 077/102] Import math was missing --- src/rat/ee_utils/ee_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rat/ee_utils/ee_utils.py b/src/rat/ee_utils/ee_utils.py index e4b345f8..cf23c6f7 100644 --- a/src/rat/ee_utils/ee_utils.py +++ b/src/rat/ee_utils/ee_utils.py @@ -1,5 +1,6 @@ import numpy as np import ee +import math # Coverts a polygon geometry object to earth engine feature def poly2feature(polygon,buffer_distance): From 0e740f3a8098b1a5b86da2953d9ab056f2d26cfe Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 13 Feb 2025 12:00:22 -0800 Subject: [PATCH 078/102] Bug Fix: Vic Memory Allocation Error coz of NaNs in Active Cells in Vic Basin Params. --- src/rat/utils/files_creator.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/rat/utils/files_creator.py b/src/rat/utils/files_creator.py index 9cf04fe0..1a946c39 100644 --- a/src/rat/utils/files_creator.py +++ b/src/rat/utils/files_creator.py @@ -61,13 +61,23 @@ def create_vic_domain_param_file(global_vic_param_file,global_vic_domain_file,ba #Inserting run_cell as mask of basin_grid in vic_param.nc basin_vic_param=gl_param.interp(lon=np.array([round_up(x,5) for x in basin_grid.lon.data ]),lat=np.array([round_up(x,5) for x in basin_grid.lat.data ]),method='nearest') + # Identify cells where 'run_cell' is NaN in original vic soil params (coz of oceanic region) + mask = basin_vic_param['run_cell'].isnull() + # Change run_cell to basin grid cells basin_vic_param['run_cell']=(('lat','lon'),basin_grid.data.astype('int32')) + if mask.sum()!=0: + # Make run_cell 0 over the null cells of original vic soil params (oceanic regions) + basin_vic_param['run_cell'] = basin_vic_param['run_cell'].where(~mask, 0) + #Saving vic_param.nc basin_vic_param.to_netcdf(os.path.join(output_dir_path,'vic_soil_param.nc')) - #Inserting run_cell as mask of basin_grid in vic_param.nc + #Inserting mask as mask of basin_grid in vic_param.nc basin_vic_domain=gl_domain.interp(lon=np.array([round_up(x,5) for x in basin_grid.lon.data ]),lat=np.array([round_up(x,5) for x in basin_grid.lat.data ]),method='nearest') basin_vic_domain['mask']=(('lat','lon'),basin_grid.data.astype('int32')) + if mask.sum()!=0: + # Make mask 0 over the null cells of original vic soil params (oceanic regions) + basin_vic_domain['mask'] = basin_vic_domain['mask'].where(~mask, 0) #Saving vic_domain.nc basin_vic_domain.to_netcdf(os.path.join(output_dir_path,'vic_domain.nc')) From 197bedff2bedf75a68dc5332ad2263790a84e073 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 13 Feb 2025 12:00:46 -0800 Subject: [PATCH 079/102] Bug Fix: Handling infinities before normalizing SSC --- .../core/sarea/multisensor_ssc_integrator.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/rat/core/sarea/multisensor_ssc_integrator.py b/src/rat/core/sarea/multisensor_ssc_integrator.py index be058c24..a2143170 100644 --- a/src/rat/core/sarea/multisensor_ssc_integrator.py +++ b/src/rat/core/sarea/multisensor_ssc_integrator.py @@ -1,3 +1,4 @@ +import numpy as np import pandas as pd import os from sklearn.preprocessing import MinMaxScaler @@ -127,15 +128,19 @@ def normalize_ssc(df): non_na_data = df['water_nir_red_mean'].dropna().values.reshape(-1, 1) df.loc[df['water_nir_red_mean'].notna(), 'nssc_nr_rd_px'] = scaler.fit_transform(non_na_data).flatten() - # Calculate reservoir-level SSC ratios and normalize, ignoring NaNs - valid_ratio_rd_gn = (df['water_red_sum'] / df['water_green_sum']).dropna() - if valid_ratio_rd_gn.any(): - scaled_data = scaler.fit_transform(valid_ratio_rd_gn.values.reshape(-1, 1)).flatten() + ## Calculate reservoir-level SSC ratios and normalize, ignoring NaNs + valid_ratio_rd_gn = (df['water_red_sum'] / df['water_green_sum']) + # Replace infinities with NaN and drop NaNs + valid_ratio_rd_gn = valid_ratio_rd_gn.replace([np.inf, -np.inf], np.nan).dropna() + # Ensure there are valid values before scaling + if not valid_ratio_rd_gn.empty: + scaled_data = scaler.fit_transform(valid_ratio_rd_gn.to_numpy().reshape(-1, 1)).flatten() df.loc[valid_ratio_rd_gn.index, 'nssc_rd_gn_res'] = scaled_data - valid_ratio_nr_rd = (df['water_nir_sum'] / df['water_red_sum']).dropna() - if valid_ratio_nr_rd.any(): - scaled_data = scaler.fit_transform(valid_ratio_nr_rd.values.reshape(-1, 1)).flatten() + valid_ratio_nr_rd = (df['water_nir_sum'] / df['water_red_sum']) + valid_ratio_nr_rd = valid_ratio_nr_rd.replace([np.inf, -np.inf], np.nan).dropna() + if not valid_ratio_nr_rd.empty: + scaled_data = scaler.fit_transform(valid_ratio_nr_rd.to_numpy().reshape(-1, 1)).flatten() df.loc[valid_ratio_nr_rd.index, 'nssc_nr_rd_res'] = scaled_data return df \ No newline at end of file From 4c6604b6bd1f296860141e6e54c2fe690239f237 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 13 Feb 2025 18:27:48 -0800 Subject: [PATCH 080/102] simplification of complex geometries for processing in AEC step as well --- src/rat/core/run_sarea.py | 4 ++-- src/rat/ee_utils/ee_aec_file_creator.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/rat/core/run_sarea.py b/src/rat/core/run_sarea.py index 3d06ef2e..420e6169 100644 --- a/src/rat/core/run_sarea.py +++ b/src/rat/core/run_sarea.py @@ -67,9 +67,9 @@ def run_sarea(start_date, end_date, sarea_save_dir, reservoirs_shpfile, shpfile_ else: bot_filter(sarea_save_dir,shpfile_column_dict,reservoirs_shpfile,**filt_options) -def run_sarea_for_res(reservoir_name, reservoir_area, reservoir_polygon, start_date, end_date, sarea_save_dir, nssc_save_dir, simplication=True): +def run_sarea_for_res(reservoir_name, reservoir_area, reservoir_polygon, start_date, end_date, sarea_save_dir, nssc_save_dir, simplification=True): - if simplication: + if simplification: # Below function simplifies geometry with shape index (complexity) higher than a threshold, otherwise original geometry is retained reservoir_polygon = simplify_geometry(reservoir_polygon) diff --git a/src/rat/ee_utils/ee_aec_file_creator.py b/src/rat/ee_utils/ee_aec_file_creator.py index 36d129fd..3a94c886 100644 --- a/src/rat/ee_utils/ee_aec_file_creator.py +++ b/src/rat/ee_utils/ee_aec_file_creator.py @@ -4,12 +4,13 @@ import pandas as pd import numpy as np from itertools import zip_longest,chain -from rat.ee_utils.ee_utils import poly2feature +from rat.ee_utils.ee_utils import poly2feature, simplify_geometry from pathlib import Path from scipy.optimize import minimize from scipy.integrate import cumulative_trapezoid from shapely.geometry import Point + WATER_SAREA_DIFF_Z_THRESHOLD = 3.0 BUFFER_DIST = 500 DEM = ee.Image('USGS/SRTMGL1_003') @@ -412,7 +413,7 @@ def extrapolate_reservoir( return predicted_storage_df -def get_obs_aec_srtm(aec_dir_path, scale, reservoir, reservoir_name, clip_to_water_surf=False): +def get_obs_aec_srtm(aec_dir_path, scale, reservoir, reservoir_name, clip_to_water_surf=False, simplification=True): """ Generates an observed Area-Elevation Curve (AEC) file for a given reservoir using SRTM data. The function checks if an AEC file already exists for the given reservoir. If it does, it reads the file and returns the data. @@ -424,6 +425,7 @@ def get_obs_aec_srtm(aec_dir_path, scale, reservoir, reservoir_name, clip_to_wat reservoir (object): The reservoir object containing geometry information. reservoir_name (str): The name of the reservoir. clip_to_water_surf (bool): If True, clips the AEC data to elevations above the water surface. Default is False. + simplification (bool): If true, reservoir geometry will be simplified before use (only if shape index is extremely high). Default is True. Returns: pd.DataFrame: A DataFrame containing the elevation and cumulative area data. @@ -444,6 +446,9 @@ def get_obs_aec_srtm(aec_dir_path, scale, reservoir, reservoir_name, clip_to_wat clip_to_water_surf = False # in this case, clipping is not required. set it to false else: reservoir_polygon = reservoir.geometry + if simplification: + # Below function simplifies geometry with shape index (complexity) higher than a threshold, otherwise original geometry is retained + reservoir_polygon = simplify_geometry(reservoir_polygon) aoi = poly2feature(reservoir_polygon,BUFFER_DIST).geometry() min_elev = DEM.reduceRegion( reducer = ee.Reducer.min(), geometry = aoi, From ce04767c77b51d3b3c024784da7f4b744beba014 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 28 Feb 2025 18:42:14 -0800 Subject: [PATCH 081/102] Bug Fix: Only those reservoir geometries will be selected which are within the basin for basin reservoir shapefile. --- src/rat/utils/files_creator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rat/utils/files_creator.py b/src/rat/utils/files_creator.py index 1a946c39..e929932b 100644 --- a/src/rat/utils/files_creator.py +++ b/src/rat/utils/files_creator.py @@ -168,11 +168,11 @@ def create_basin_reservoir_shpfile(reservoir_shpfile,reservoir_shpfile_column_di reservoirs_gdf_column_dict = reservoir_shpfile_column_dict if routing_station_global_data: - reservoirs_spatialjoin = gpd.sjoin(reservoirs, basin_data_crs_changed, "inner")[reservoirs.columns] + reservoirs_spatialjoin = gpd.sjoin(reservoirs, basin_data_crs_changed, predicate="within", how="inner")[reservoirs.columns] reservoirs_spatialjoin['uniq_id'] = reservoirs_spatialjoin[reservoirs_gdf_column_dict['id_column']].astype(str)+'_'+ \ reservoirs_spatialjoin[reservoirs_gdf_column_dict['dam_name_column']].astype(str).str.replace(' ','_') else: - reservoirs_spatialjoin = gpd.sjoin(reservoirs, basin_data_crs_changed, "inner")[reservoirs.columns] + reservoirs_spatialjoin = gpd.sjoin(reservoirs, basin_data_crs_changed, predicate="within", how="inner")[reservoirs.columns] if(reservoirs_spatialjoin.empty): raise Exception('Reservoir names in reservoir shapefile are not matching with the station names in the station file used for routing.') From eab5005feef30b39b16ef65b086d65b9d4652a02 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sat, 1 Mar 2025 17:36:35 -0800 Subject: [PATCH 082/102] Bug Fix: Added MaxPixels for AEC estimation to avoid error --- src/rat/ee_utils/ee_aec_file_creator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rat/ee_utils/ee_aec_file_creator.py b/src/rat/ee_utils/ee_aec_file_creator.py index 3a94c886..6c7efdf8 100644 --- a/src/rat/ee_utils/ee_aec_file_creator.py +++ b/src/rat/ee_utils/ee_aec_file_creator.py @@ -37,7 +37,9 @@ def _aec(n,elev_dem,roi, scale=30): DEM141Count = DEM141.reduceRegion( geometry= roi, scale= scale, - reducer= ee.Reducer.sum() + reducer= ee.Reducer.sum(), + maxPixels = 1e16, + bestEffort=True ) area=ee.Number(DEM141Count.get('elevation')).multiply(30*30).divide(1e6) return area From 51b97603721b7ac327f707be5904bcd91d475e54 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 14 Mar 2025 21:55:12 -0700 Subject: [PATCH 083/102] Added usage of optical data only when Sar based correction fails. --- src/rat/core/sarea/TMS.py | 132 ++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 61 deletions(-) diff --git a/src/rat/core/sarea/TMS.py b/src/rat/core/sarea/TMS.py index 426f9e73..96c187fc 100644 --- a/src/rat/core/sarea/TMS.py +++ b/src/rat/core/sarea/TMS.py @@ -290,13 +290,14 @@ def tms_os(self, sar.loc[extrapolated_date, "sarea"] = extrapolated_value sar = sar.rename({'sarea': 'area'}, axis=1) + sar_correction = 'Possible' # If SAR has less than 3 points else: - sar = None + sar_correction = 'Not Possible' print("Sentinel-1 SAR has less than 3 data points.") # If SAR file does not exist else: - sar = None + sar_correction = 'Not Possible' print("Sentinel-1 SAR file does not exist.") # combine opticals into one dataframes @@ -304,66 +305,75 @@ def tms_os(self, optical = optical.loc[~optical.index.duplicated(keep='last')] # when both s2 and l8 are present, keep s2 optical.rename({'water_area_corrected': 'area'}, axis=1, inplace=True) - + error_message = None # Apply the trend based corrections - if(sar is not None): - # If Optical begins before SAR and has a difference of more than 15 days - if(sar.index[0]-optical.index[0]>pd.Timedelta(days=15)): - # Optical without SAR - optical_with_no_sar = optical[optical.index[0]:sar.index[0]].copy() - optical_with_no_sar['non-smoothened optical area'] = optical_with_no_sar['area'] - optical_with_no_sar.loc[:, 'days_passed'] = optical.index.to_series().diff().dt.days.fillna(0) - # Calculate smoothed values with moving weighted average method if more than 7 values; weights are calculated using cloud percent. - if len(optical_with_no_sar)>7: - # Temporarily interpolate missing values - optical_with_no_sar['interpolated_area'] = optical_with_no_sar['non-smoothened optical area'].interpolate(method='linear') - - # Apply weighted moving average using interpolated values - optical_with_no_sar['smoothed_area'] = weighted_moving_average( - optical_with_no_sar['interpolated_area'], - weights=(101 - optical_with_no_sar['cloud_percent']), - window_size=3 - ) - - # Restore NaN values to preserve the original structure - optical_with_no_sar['filled_area'] = optical_with_no_sar['smoothed_area'].where( - ~optical_with_no_sar['non-smoothened optical area'].isna() - ) - - # Drop temporary columns - optical_with_no_sar = optical_with_no_sar.drop(['interpolated_area', 'smoothed_area'], axis=1) - - # Drop 'area' column from optical_with_no_sar - optical_with_no_sar = optical_with_no_sar.drop('area',axis=1) - # Optical with SAR - optical_with_sar = trend_based_correction(optical.copy(), sar.copy(), self.AREA_DEVIATION_THRESHOLD) - # Merge both - result = pd.concat([optical_with_no_sar,optical_with_sar],axis=0) - # Smoothen the combined surface area estimates to avoid noise or peaks using savgol_filter if more than 9 values (to increase smoothness and include more points as we have both TMS-OS and Optical) - if len(result)>9: - # Temporarily interpolate missing values for smoothing - result['interpolated_area'] = result['filled_area'].interpolate(method='linear') - - # Apply Savitzky-Golay filter - result['smoothed_filled_area'] = savgol_filter( - result['interpolated_area'], window_length=7, polyorder=3 - ) - - # Restore NaN values - filled_area_with_nans = result['smoothed_filled_area'].where( - ~result['filled_area'].isna() - ) - result['filled_area'] = filled_area_with_nans - - # Drop temporary columns - result = result.drop(['interpolated_area', 'smoothed_filled_area'], axis=1) - - method = 'Combine' - # If SAR begins before Optical - else: - result = trend_based_correction(optical.copy(), sar.copy(), self.AREA_DEVIATION_THRESHOLD) - method = 'TMS-OS' - else: + if(sar_correction == 'Possible'): + try: + # If Optical begins before SAR and has a difference of more than 15 days + if(sar.index[0]-optical.index[0]>pd.Timedelta(days=15)): + # Optical without SAR + optical_with_no_sar = optical[optical.index[0]:sar.index[0]].copy() + optical_with_no_sar['non-smoothened optical area'] = optical_with_no_sar['area'] + optical_with_no_sar.loc[:, 'days_passed'] = optical.index.to_series().diff().dt.days.fillna(0) + # Calculate smoothed values with moving weighted average method if more than 7 values; weights are calculated using cloud percent. + if len(optical_with_no_sar)>7: + # Temporarily interpolate missing values + optical_with_no_sar['interpolated_area'] = optical_with_no_sar['non-smoothened optical area'].interpolate(method='linear') + + # Apply weighted moving average using interpolated values + optical_with_no_sar['smoothed_area'] = weighted_moving_average( + optical_with_no_sar['interpolated_area'], + weights=(101 - optical_with_no_sar['cloud_percent']), + window_size=3 + ) + + # Restore NaN values to preserve the original structure + optical_with_no_sar['filled_area'] = optical_with_no_sar['smoothed_area'].where( + ~optical_with_no_sar['non-smoothened optical area'].isna() + ) + + # Drop temporary columns + optical_with_no_sar = optical_with_no_sar.drop(['interpolated_area', 'smoothed_area'], axis=1) + + # Drop 'area' column from optical_with_no_sar + optical_with_no_sar = optical_with_no_sar.drop('area',axis=1) + # Optical with SAR + optical_with_sar = trend_based_correction(optical.copy(), sar.copy(), self.AREA_DEVIATION_THRESHOLD) + # Merge both + result = pd.concat([optical_with_no_sar,optical_with_sar],axis=0) + # Smoothen the combined surface area estimates to avoid noise or peaks using savgol_filter if more than 9 values (to increase smoothness and include more points as we have both TMS-OS and Optical) + if len(result)>9: + # Temporarily interpolate missing values for smoothing + result['interpolated_area'] = result['filled_area'].interpolate(method='linear') + + # Apply Savitzky-Golay filter + result['smoothed_filled_area'] = savgol_filter( + result['interpolated_area'], window_length=7, polyorder=3 + ) + + # Restore NaN values + filled_area_with_nans = result['smoothed_filled_area'].where( + ~result['filled_area'].isna() + ) + result['filled_area'] = filled_area_with_nans + + # Drop temporary columns + result = result.drop(['interpolated_area', 'smoothed_filled_area'], axis=1) + + method = 'Combine' + # If SAR begins before Optical + + else: + result = trend_based_correction(optical.copy(), sar.copy(), self.AREA_DEVIATION_THRESHOLD) + method = 'TMS-OS' + except Exception as e: + sar_correction = 'Failed' + error_message = e + + if sar_correction=='Failed' or sar_correction=='Not Possible': + if sar_correction=='Failed': + print(f'SAR trend based correction (TMS-OS) failed due to {error_message}. Using only optical data.') + result = optical.copy() result['non-smoothened optical area'] = result['area'] result.loc[:, 'days_passed'] = optical.index.to_series().diff().dt.days.fillna(0) From 3deb2d223be7fbea696002b42fdf7be6435e9718 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 10 Apr 2025 14:03:01 -0700 Subject: [PATCH 084/102] Bug Fix: Reading of large meteorlogical combined NetCDF files used to give error --- src/rat/toolbox/data_transform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rat/toolbox/data_transform.py b/src/rat/toolbox/data_transform.py index b778e59b..9912ccc1 100644 --- a/src/rat/toolbox/data_transform.py +++ b/src/rat/toolbox/data_transform.py @@ -66,7 +66,7 @@ def create_meterological_ts(roi, nc_file_path, output_csv_path): print("Creating meterological timeseries for a given geometry using comibined meteorlogical NetCDF produced by RAT.") if os.path.isfile(nc_file_path): # Load the NetCDF file as an xarray Dataset - ds = xr.open_dataset(nc_file_path) + ds = xr.open_dataset(nc_file_path, chunks={"time": 100, "x": 100, "y": 100}) # Ensure spatial dimensions are set correctly as x and y for rioxarray use if 'lon' in ds.dims and 'lat' in ds.dims: @@ -99,7 +99,7 @@ def create_meterological_ts(roi, nc_file_path, output_csv_path): print("Catchment ROI is sufficiently large, no buffer applied.") roi_expanded = roi # No buffer needed, keep original ROI else: - raise ValueError("Catchment ROI must be a polygon or a mulipolygon.") + raise ValueError("Catchment ROI must be a polygon or a multipolygon.") # Sets default CRS for the dataset if missing if ds.rio.crs is None: From 306d2c8bc1d3319215bf6fe3812ddd402a4cfb47 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 10 Apr 2025 14:03:48 -0700 Subject: [PATCH 085/102] Bug Fix: If error was other than computation time out, it used to stuck in infinite loop. --- src/rat/core/sarea/sarea_cli_s2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/rat/core/sarea/sarea_cli_s2.py b/src/rat/core/sarea/sarea_cli_s2.py index a3ee2b22..e2d06041 100644 --- a/src/rat/core/sarea/sarea_cli_s2.py +++ b/src/rat/core/sarea/sarea_cli_s2.py @@ -583,6 +583,8 @@ def run_process_long(res_name,res_polygon, start, end, datadir, results_per_iter scale_to_use = SPATIAL_SCALE_MEDIUM success_status = -1 break + else: + success_status = -1 else: success_status = 1 if success_status==1: From 5a4723ff1216718a6c1122cd76c20c6b5f0027c1 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Thu, 10 Apr 2025 14:06:05 -0700 Subject: [PATCH 086/102] Bug Fix: If reservoir is large and contains a lot of grid points, evaporation used to give error. Made code efficient. --- src/rat/core/run_postprocessing.py | 40 ++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/rat/core/run_postprocessing.py b/src/rat/core/run_postprocessing.py index 25f489a8..5c12a0b4 100644 --- a/src/rat/core/run_postprocessing.py +++ b/src/rat/core/run_postprocessing.py @@ -3,6 +3,7 @@ import pandas as pd import numpy as np import xarray as xr +import rioxarray as rxr import geopandas as gpd import warnings import datetime @@ -80,6 +81,7 @@ def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, s log.debug("Checking if grid cells lie inside reservoir") last_layer = reqvars.isel(time=-1).to_dataframe().reset_index() temp_gdf = gpd.GeoDataFrame(last_layer, geometry=gpd.points_from_xy(last_layer.lon, last_layer.lat)) + log.debug('Calculating points within') points_within = temp_gdf[temp_gdf.within(res_geom)]['geometry'] if len(points_within) == 0: @@ -94,10 +96,32 @@ def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, s P.head() else: - # print(f"[!] {len(points_within)} Grid cells inside reservoir found, averaging their values") - data = reqvars.sel(lat=np.array(points_within.y), lon=np.array(points_within.x), method='nearest').to_dataframe().reset_index().groupby('time').mean()[1:] - - P = forcings.sel(lat=np.array(points_within.y), lon=np.array(points_within.x), method='nearest').resample({'time':'1D'}).mean().to_dataframe().groupby('time').mean()[1:] + log.debug(f"{len(points_within)} Grid cells found inside reservoir, averaging their values") + # data = reqvars.sel(lat=np.array(points_within.y), lon=np.array(points_within.x), method='nearest').to_dataframe().reset_index().groupby('time').mean()[1:] + # Convert dataset to a spatial-aware dataset + reqvars = reqvars.rio.write_crs("EPSG:4326") # Ensure correct CRS + # Clip using geometry + reqvars_roi_clip = reqvars.rio.clip([res_geom], reqvars.rio.crs, drop=True) + # reqvars_roi_clip = reqvars.sel(lat=np.array(points_within.y), lon=np.array(points_within.x), method='nearest') + # Averaging the data + data = reqvars_roi_clip.mean(dim=['lat','lon']).to_dataframe().reset_index()[1:].set_index('time') + forcings = forcings.rio.write_crs("EPSG:4326") + # Ensure dataset has proper spatial dimensions + forcings_xy = forcings.rename({'lon': 'x', 'lat': 'y'}) + forcings_roi_clip = forcings_xy.rio.clip([res_geom], reqvars.rio.crs, drop=True) + # forcings_roi_clip = forcings.sel(lat=np.array(points_within.y), lon=np.array(points_within.x), method='nearest') + P = ( + forcings_roi_clip.mean(dim=['x', 'y']) # Average over spatial dimensions + .resample(time='1D').mean() # Resample first! + .to_dataframe() # Convert to DataFrame + .reset_index()[1:] + .set_index('time') # Optional: Remove first row if necessary + ) + + P = P['air_pressure'] + # P = forcings_roi_clip.mean(dim=['lat','lon']).to_dataframe().resample(time='1D').reset_index()[1:] + # P = P['air_pressure'] + # P = forcings.sel(lat=np.array(points_within.y), lon=np.array(points_within.x), method='nearest').resample({'time':'1D'}).mean().to_dataframe().groupby('time').mean()[1:] data['P'] = P # If no vic result file exists then check if data is there in existing file between start and end time @@ -127,7 +151,13 @@ def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, s ix = pd.date_range(start=first_obs, end=end_date, freq='D') sarea_interpolated = sarea_interpolated.reindex(ix).fillna(method='ffill') # Slicing is exclusive of end date. But we want inclusive of both start and end dates. - sarea_interpolated = sarea_interpolated.loc[sarea_interpolated.index.isin(data.index)] + # sarea_interpolated.index = pd.to_datetime(sarea_interpolated.index) + # data.index = pd.to_datetime(data.index) + # print("Sarea Data:", sarea_interpolated.head(5)) + # print(sarea_interpolated.index.isin(data.index)) + # print(data.index.equals(sarea_interpolated.index)) + # sarea_interpolated = sarea_interpolated.loc[sarea_interpolated.index.isin(data.index)] + sarea_interpolated = sarea_interpolated.reindex(data.index) if isinstance(sarea, pd.DataFrame): data['area'] = sarea_interpolated['area'] From 1527bc2b83f6fa9b5783f50f3c35831b18c76af9 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 4 Jul 2025 18:44:20 -0700 Subject: [PATCH 087/102] Asc file will be vreated directly from tif file if present in step 7 for flow direction --- src/rat/utils/files_creator.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/rat/utils/files_creator.py b/src/rat/utils/files_creator.py index e929932b..5d376221 100644 --- a/src/rat/utils/files_creator.py +++ b/src/rat/utils/files_creator.py @@ -82,17 +82,18 @@ def create_vic_domain_param_file(global_vic_param_file,global_vic_domain_file,ba basin_vic_domain.to_netcdf(os.path.join(output_dir_path,'vic_domain.nc')) def create_basin_grid_flow_asc(global_flow_grid_dir_tif, basingridfile_path, savepath, flow_direction_replace_dict = None): - global_flow_grid_dir=rxr.open_rasterio(global_flow_grid_dir_tif) - basin_grid=rxr.open_rasterio(basingridfile_path) - basin_flow_grid_dir = global_flow_grid_dir.interp(x=np.array([round_up(i,5) for i in basin_grid.x.data ]), - y=np.array([round_up(i,5) for i in basin_grid.y.data ]),method='nearest') - if (flow_direction_replace_dict): - for i in flow_direction_replace_dict: - basin_flow_grid_dir = basin_flow_grid_dir.where(basin_flow_grid_dir!=i, flow_direction_replace_dict[i]) - - basin_flow_grid_dir = basin_flow_grid_dir.rio.write_nodata(0) - basin_flow_grid_dir = basin_flow_grid_dir.where(basin_grid.data==1,0) - basin_flow_grid_dir.rio.to_raster(savepath+'.tif', dtype='int16') + if not os.path.exists(savepath+'.tif'): + global_flow_grid_dir=rxr.open_rasterio(global_flow_grid_dir_tif) + basin_grid=rxr.open_rasterio(basingridfile_path) + basin_flow_grid_dir = global_flow_grid_dir.interp(x=np.array([round_up(i,5) for i in basin_grid.x.data ]), + y=np.array([round_up(i,5) for i in basin_grid.y.data ]),method='nearest') + if (flow_direction_replace_dict): + for i in flow_direction_replace_dict: + basin_flow_grid_dir = basin_flow_grid_dir.where(basin_flow_grid_dir!=i, flow_direction_replace_dict[i]) + + basin_flow_grid_dir = basin_flow_grid_dir.rio.write_nodata(0) + basin_flow_grid_dir = basin_flow_grid_dir.where(basin_grid.data==1,0) + basin_flow_grid_dir.rio.to_raster(savepath+'.tif', dtype='int16') # Change format, and save as asc file cmd = [ From 85ef4efe79cd522d981b82eed3f1bde7623373fa Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 4 Jul 2025 18:45:26 -0700 Subject: [PATCH 088/102] removed unnecessary parameters & changed aec path to the ones in rat outputs in step 13 --- src/rat/rat_basin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rat/rat_basin.py b/src/rat/rat_basin.py index c61c09e9..a2f38834 100644 --- a/src/rat/rat_basin.py +++ b/src/rat/rat_basin.py @@ -779,7 +779,7 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base rat_logger.warning("AEC files could not be copied to rat_outputs directory.", exc_info=True) #Generating evaporation, storage change and outflow. DELS_STATUS, EVAP_STATUS, OUTFLOW_STATUS = run_postprocessing(basin_name, basin_data_dir, basin_reservoir_shpfile_path, reservoirs_gdf_column_dict, - aec_dir_path, config['BASIN']['start'], config['BASIN']['end'], rout_init_state_save_file, use_state, evap_savedir, dels_savedir, + aec_savedir, config['BASIN']['start'], config['BASIN']['end'], rout_init_state_save_file, use_state, evap_savedir, dels_savedir, nssc_savedir, outflow_savedir, VIC_STATUS, ROUTING_STATUS, GEE_STATUS, forecast_mode=forecast_mode) except: no_errors = no_errors+1 @@ -804,7 +804,7 @@ def rat_basin(config, rat_logger, forecast_mode=False, gfs_days=0, forecast_base ## Inflow if(ROUTING_STATUS): - convert_inflow(inflow_dst_dir, basin_reservoir_shpfile_path, reservoirs_gdf_column_dict, final_output_path) + convert_inflow(inflow_dst_dir, final_output_path) rat_logger.info("Converted Inflow to the Output Format.") else: rat_logger.info("Could not convert Inflow to the Output Format as Routing run failed.") From 2f21abaf609b018fde42a22ba676f40cfa95d559 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 4 Jul 2025 18:45:58 -0700 Subject: [PATCH 089/102] Step 13 will noe throw Exception if reservoir shapefile is not present. --- src/rat/core/run_postprocessing.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rat/core/run_postprocessing.py b/src/rat/core/run_postprocessing.py index 5c12a0b4..f7801ae0 100644 --- a/src/rat/core/run_postprocessing.py +++ b/src/rat/core/run_postprocessing.py @@ -234,7 +234,10 @@ def run_postprocessing(basin_name, basin_data_dir, reservoir_shpfile, reservoir_ evap_datadir, dels_savedir, nssc_savedir, outflow_savedir, vic_status, routing_status, gee_status, forecast_mode=False): # read file defining mapped resrvoirs # reservoirs_fn = os.path.join(project_dir, 'backend/data/ancillary/RAT-Reservoirs.geojson') - reservoirs = gpd.read_file(reservoir_shpfile) + if os.path.isfile(reservoir_shpfile): + reservoirs = gpd.read_file(reservoir_shpfile) + else: + raise Exception("Reservoir Shapefile is not avaialble. Evaporation, Storage change and Outflow cannot be calculated.") start_date_str = start_date.strftime("%Y-%m-%d") if(use_rout_state): start_date_str_evap = (start_date-datetime.timedelta(days=100)).strftime("%Y-%m-%d") From e02701f3a85a92439404713a8c65e8048b646fe2 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Fri, 4 Jul 2025 18:46:30 -0700 Subject: [PATCH 090/102] removed unnecessary parameters and its related code: not required --- src/rat/utils/convert_to_final_outputs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/rat/utils/convert_to_final_outputs.py b/src/rat/utils/convert_to_final_outputs.py index 634e10b8..521e15bb 100644 --- a/src/rat/utils/convert_to_final_outputs.py +++ b/src/rat/utils/convert_to_final_outputs.py @@ -24,10 +24,8 @@ def convert_sarea(sarea_dir, website_v_dir): df.to_csv(savepath, index=False) -def convert_inflow(inflow_dir, reservoir_shpfile, reservoir_shpfile_column_dict, final_out_dir): +def convert_inflow(inflow_dir, final_out_dir): # Inflow - reservoirs = gpd.read_file(reservoir_shpfile) - reservoirs['Inflow_filename'] = reservoirs[reservoir_shpfile_column_dict['unique_identifier']].astype(str) inflow_paths = list(Path(inflow_dir).glob('*.csv')) final_out_inflow_dir = Path(final_out_dir) / 'inflow' From 8d7a44545636d14dda50c07d7efee56a85de7f49 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sat, 16 Aug 2025 21:12:00 -0700 Subject: [PATCH 091/102] Bug Fix: Handled cases when evaporation is calculated for same time period --- src/rat/core/run_postprocessing.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/rat/core/run_postprocessing.py b/src/rat/core/run_postprocessing.py index f7801ae0..f594e013 100644 --- a/src/rat/core/run_postprocessing.py +++ b/src/rat/core/run_postprocessing.py @@ -176,7 +176,11 @@ def calc_E(res_data, start_date, end_date, forcings_path, vic_res_path, sarea, s # Save (Writing new file if not exist otherwise append) if existing_data is not None: - new_data = data.copy() + new_data = data.copy().reset_index(drop=True) + existing_data = existing_data.reset_index(drop=True) + # Remove duplicate columns for concatenation + existing_data = existing_data.loc[:, ~existing_data.columns.duplicated()] + new_data = new_data.loc[:, ~new_data.columns.duplicated()] # Concat the two dataframes into a new dataframe holding all the data (memory intensive): complement = pd.concat([existing_data, new_data], ignore_index=True) # Remove all duplicates: @@ -348,7 +352,7 @@ def run_postprocessing(basin_name, basin_data_dir, reservoir_shpfile, reservoir_ no_failed_files +=1 EVAP_STATUS = 1 if no_failed_files: - log_level1.warning(f"Evapotration was not calculated for {no_failed_files} reservoir(s). Please check Level-2 log file for more details.") + log_level1.warning(f"Evaporation was not calculated for {no_failed_files} reservoir(s). Please check Level-2 log file for more details.") elif((not vic_status) and (not gee_status)): log.debug("Cannot Retrieve Evaporation because both VIC and GEE Run Failed.") elif(vic_status): From 1cbc74dde0033c0994032d807dae1586a4eb1e77 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sat, 16 Aug 2025 21:18:22 -0700 Subject: [PATCH 092/102] Bug Fix: Handled case when id column is not given for reservoir shapefile. --- src/rat/core/run_sarea.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/rat/core/run_sarea.py b/src/rat/core/run_sarea.py index 420e6169..673d7ed7 100644 --- a/src/rat/core/run_sarea.py +++ b/src/rat/core/run_sarea.py @@ -31,7 +31,12 @@ def run_sarea(start_date, end_date, sarea_save_dir, reservoirs_shpfile, shpfile_ Partial_optical_tmsos_files = 0 i = 1 for reservoir_no,reservoir in reservoirs_polygon.iterrows(): - print(f"\n\n +++ PROCESSING RESERVOIR: {reservoir[shpfile_column_dict['id_column']]} - {reservoir[shpfile_column_dict['dam_name_column']]} ({i}/{len(reservoirs_polygon)}) +++\n\n") + # printing reservoir id & name (whatever available) + if shpfile_column_dict.get('id_column'): + print(f"\n\n +++ PROCESSING RESERVOIR: {reservoir[shpfile_column_dict['id_column']]} - {reservoir[shpfile_column_dict['dam_name_column']]} ({i}/{len(reservoirs_polygon)}) +++\n\n") + else: + print(f"\n\n +++ PROCESSING RESERVOIR: {reservoir[shpfile_column_dict['dam_name_column']]} ({i}/{len(reservoirs_polygon)}) +++\n\n") + i += 1 try: # Reading reservoir information From 6bf8a3cfb198ec0e0627e090523c8ed85c9d35c9 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sat, 16 Aug 2025 21:19:05 -0700 Subject: [PATCH 093/102] Bug Fix: Handled case when dam height column is not provided in reservoir shapefile. --- src/rat/ee_utils/ee_aec_file_creator.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/rat/ee_utils/ee_aec_file_creator.py b/src/rat/ee_utils/ee_aec_file_creator.py index 6c7efdf8..1bad89db 100644 --- a/src/rat/ee_utils/ee_aec_file_creator.py +++ b/src/rat/ee_utils/ee_aec_file_creator.py @@ -542,8 +542,11 @@ def aec_file_creator( reservoir_gpd = reservoir_gpd.set_crs(reservoirs_polygon.crs) reservoir_name = str(reservoir[shpfile_column_dict['unique_identifier']]) - if reservoir[shpfile_column_dict['dam_height']] is not None: - dam_height = float(reservoir[shpfile_column_dict['dam_height']]) + if shpfile_column_dict.get('dam_height'): + if reservoir[shpfile_column_dict['dam_height']] is not None: + dam_height = float(reservoir[shpfile_column_dict['dam_height']]) + else: + dam_height = np.nan else: dam_height = np.nan dam_lat = float(reservoir[shpfile_column_dict['dam_lat']]) From d9ac11841ed0b7e52d4ba76431de3ee58ca9a3fd Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sat, 16 Aug 2025 23:27:39 -0700 Subject: [PATCH 094/102] Bug Fix: Code corrected to skip AEC Extrapolation in case dam location was not available. --- src/rat/ee_utils/ee_aec_file_creator.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/rat/ee_utils/ee_aec_file_creator.py b/src/rat/ee_utils/ee_aec_file_creator.py index 1bad89db..8fbcfd3b 100644 --- a/src/rat/ee_utils/ee_aec_file_creator.py +++ b/src/rat/ee_utils/ee_aec_file_creator.py @@ -549,17 +549,25 @@ def aec_file_creator( dam_height = np.nan else: dam_height = np.nan - dam_lat = float(reservoir[shpfile_column_dict['dam_lat']]) - dam_lon = float(reservoir[shpfile_column_dict['dam_lon']]) - dam_location = Point(dam_lon, dam_lat) + + # + dam_lat = float(reservoir[shpfile_column_dict.get('dam_lat')]) if shpfile_column_dict.get('dam_lat') else None + dam_lon = float(reservoir[shpfile_column_dict.get('dam_lon')]) if shpfile_column_dict.get('dam_lat') else None + if dam_lat and dam_lon: + dam_location = Point(dam_lon, dam_lat) + else: + dam_location = None aec, water_surface_exists = get_obs_aec_srtm(aec_dir_path, scale, reservoir, reservoir_name, clip_to_water_surf=True) if water_surface_exists: - extrapolate_reservoir( - reservoir_gpd, dam_location, reservoir_name, dam_height, aec, - aec_dir_path, grwl_fp=grwl_fp - ) + if dam_location: + extrapolate_reservoir( + reservoir_gpd, dam_location, reservoir_name, dam_height, aec, + aec_dir_path, grwl_fp=grwl_fp + ) + else: + print(f"No extrapolation was done in AEC for reservoir {reservoir_name} because of absence of absence of Dam Location. Reservoir's bottom elevation cannot be determined.") else: print(f"No extrapolation was done in AEC for reservoir {reservoir_name} because of absence of water surface in AEC.") From 672f9f1e5781725e138d66864ac544223ccae6bd Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 17 Aug 2025 00:31:49 -0700 Subject: [PATCH 095/102] Added NSSC as a different test. --- src/rat/cli/rat_test_verify.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/rat/cli/rat_test_verify.py b/src/rat/cli/rat_test_verify.py index e4856759..51eacf62 100644 --- a/src/rat/cli/rat_test_verify.py +++ b/src/rat/cli/rat_test_verify.py @@ -27,6 +27,9 @@ def verify_test_results(self): print("############################## Test-6 (Surface Area) ##############################",end="\n") self._verify_test_results_for_var('sarea_tmsos','Surface Area') + + print("############################## Test-7 (NSSC) ##############################",end="\n") + self._verify_test_results_for_var('nssc','Normalized Suspended Sediment Concentration') # Comparing true and estimated files for a variable def _verify_test_results_for_var(self, var_to_observe, var_to_display): From f0f1eae16eed0f981d3ce3c88a59f6a01abb9170 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 17 Aug 2025 00:32:47 -0700 Subject: [PATCH 096/102] Bug Fix: Catchment Climate should throw warning if no catchment file is available. --- src/rat/utils/convert_to_final_outputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rat/utils/convert_to_final_outputs.py b/src/rat/utils/convert_to_final_outputs.py index 521e15bb..2b1614d6 100644 --- a/src/rat/utils/convert_to_final_outputs.py +++ b/src/rat/utils/convert_to_final_outputs.py @@ -180,7 +180,7 @@ def convert_meteorological_ts(catchment_shpfile, catchments_gdf_column_dict, bas return failed_res_no else: - return + return 'all' def convert_v2_frontend(basin_data_dir, res_name, inflow_src, sarea_src, dels_src, outflow_src): From 16985193d2cc378f35758fc922500928d6618012 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 17 Aug 2025 00:49:33 -0700 Subject: [PATCH 097/102] Updated google drive link & test data expected outputs for new Sarea and AEC changes --- src/rat/cli/rat_init_config.py | 1 + src/rat/cli/rat_test_config.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rat/cli/rat_init_config.py b/src/rat/cli/rat_init_config.py index f966220a..188b2f52 100644 --- a/src/rat/cli/rat_init_config.py +++ b/src/rat/cli/rat_init_config.py @@ -5,6 +5,7 @@ 'global_vic_params': "https://www.dropbox.com/s/jsg2wu62qi2ltwz/global_vic_params.zip?dl=1", } +## For google drive link, https://drive.google.com/file/d//view?usp=sharing, use https://drive.google.com/uc?id= DOWNLOAD_LINKS_GOOGLE = { 'route_model': "https://drive.google.com/uc?id=1zr3VH0wy-XN-yF2_n0xic89_PiT_V_Mb", 'params': "https://drive.google.com/uc?id=1LAGivWvgBdtJvDWzkGKorzfjPEKvO69k", diff --git a/src/rat/cli/rat_test_config.py b/src/rat/cli/rat_test_config.py index fa1ace16..b21450ae 100644 --- a/src/rat/cli/rat_test_config.py +++ b/src/rat/cli/rat_test_config.py @@ -3,8 +3,10 @@ DOWNLOAD_LINK_DROPBOX = { 'test_data': "https://www.dropbox.com/scl/fi/f1pnyz9mo178kweh3agtf/test_data.zip?dl=1&rlkey=qxvsfrhc2li55dh4yj4a03bq2" } + +## For google drive link, https://drive.google.com/file/d//view?usp=sharing, use https://drive.google.com/uc?id= DOWNLOAD_LINK_GOOGLE = { - 'test_data': "https://drive.google.com/uc?id=1F5Z6f4rX7nBSXXo7XYGK_XC0i5-G6zBF" + 'test_data': "https://drive.google.com/uc?id=1yV2zfqovPjAyWI4yWL5lhN7_GWaXL2Va" } PATHS = { From d90fbcd6e3c913504b15197f74419bfd0c009465 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 17 Aug 2025 02:40:10 -0700 Subject: [PATCH 098/102] updated test output in docs tutorial --- .../tutorials/KarnatakaFloods/test_output.png | Bin 224741 -> 152805 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/tutorials/KarnatakaFloods/test_output.png b/docs/images/tutorials/KarnatakaFloods/test_output.png index 06ee0a069fc3140612b6ee09afe7cd8d85f1ece8..b941f917151e9086ccc61317ef253220723867a6 100644 GIT binary patch literal 152805 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�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 literal 224741 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
*co6PLz-xvR#&M8>M zRDgqU-2jp!e-g}aVAK71k=X{RL=1r3?TMV2w5|qzGzGaIlC@6k`1kTIXve52 zsV$-Wb+^-lpj&v(tZa4=D z*e<%Pq#1Wly(v-dzWm4}7;veGw;P7cF}h|}Uxfn>%|M`sfqVAF%V0}Lj{+>NDp7qt z!K{W>htggGtNm^GqmZV0_>kV$DBvp!f|j_b;jJ*X3@b-}{~ktlZ}RS51+&Px9b-m{ zkS5k-P6ld52(K1JW!CPF=OlV7%LD@CSYZtTn+W=Va5rA3H<$DZk-GQ{EvmfKrzB|_ zLvUy9_wn)#;jahN?LvRN;ipO-a_Sfy7gJAe+Gc+Lf;Ao%?{@$kmU3+s!)n4)-zN27 z0LsZ&?A-8(@~TaHa#nSAzWeWikAYfp=$5Yinx}?o)ch2$QExbKvoaJ{W_&mAL_V!7 zY>%T-{J^>=j(-uHv~g9AZ7DF&HUr~9X4Wg~W}vvpWxrZHGJ96%-cy#3214Pw>PDZ= zqrpO3>LH?x(>ePnh)|&m9hH`JFr41}b*x@Yd%e|Cg@7K_;-silgR@Hz_N8DEN)Eug zWO@$f;^vX~vzBh#G)@3G$L}Ca!%qZi@ld|k{|!h9O$TvHJF4cHy3*FbhaMt)q5_K+ zZiqasvbUxAj$=#g9%S)&y-c6BEYYML_rUkl(8Ohq*bT8FJkY@^eBEY^p@+z=YP4Ur z-FaxaHPBjW&Lo(R<_cwow9MeMcxk?m*;Arqr`Tg5dj6c;HPgHEp6Z?pxlfX*(nA{I zvr9mhp8Xgf8QAunog5m^Zcg;obE9Pb)$`K5I8MBSC1sN-SLvqULWPAH8{#@VU>QZs zZ2c(9<~aNp`KxOZ$wpF|oOBpKnqe#q1Txpt0u9#^ac;^eXX&Gho1rr! zK-Dm9^XTCb-A`E=G+c$IR39IZCro+Q~#jOg&xRS&pvw$j!c->fIYkx`axz zU{66zLQOA)87^PV9R#-N9R8SnTcK}y9wY@prTvcVykQ??e_8H#Kkj{-!~6ocbKRYC z=g%^OocoevS`!%J%`qZZz+Z|NVkuKN^tio()QFJ44RolITX{*3mR$y}?3&N#5?#B@ zqAfrQc2qqjzf>Q`^lxx2R#FEA7!Bt}mPROwZ^R#sO+>Cq3)y7||3T!8o)-VI(V#BE z@`j}e9%2>_vYiMFQ=Z23C|k|5uvRQOs!2sTY!@`}Hm_5UpM)#<q_r12mzv)A5~fKf zWn1X{8|`OsnpM8y2iJtNy#dsNzaUF!c{(ME`YjLDMvX0`J4+MGrdPBNrQH-ld#?2UtU&rf5vLv6fFz} zhVzJyt0`QY(UV+Ag#K+HsqMzcWk3JZH@EW%K(UGVQCktn)7|VknUT3A;|YoV_Jy%8 zxlp6+yd=+WtXYBs*yol6Fqd@aM>BrPEP?=yD&hm5-?-nq0!l%?$#YNR zj!p>i!G%ur`XeIZmX=Ztu3KC-v=LwVwD9ZI2l5oxybofBYFbvTyicXm1#;IUL7B2^ zF{Wu-4or#;tSwso_Z_z9yN}=0(ZO!i|L)vTA)?Tyi5%(ueQ|>!g6N^N);FOsaVIlP z`aSeu$Lts|h%|{s4qyoq&n3I@$FcgTIQ6}bP=>n+PiZVITAwW5yQA#zY-v)a_^76M z_OV9tXc@w7NbU5a$(zHE&sQi)PPWHHam3EOoJBw$W7qcN+cSkw@@9grqutKEVLSWP z@Z0x?E)K`9!A@yYh_s!j?30PT37rjI>D_SsC3364)T+?aJzvB^Gyqv;HTrrjo3zCQz z0SA9dU}+sy8&^+%@~k(^1$`M3`2?Z#Vm}KdcwLiz6mRfc-dDD6HeQ#EjgS~sEFDoa5%M~@l*x~~v>tl5U_;cGL8SzU0 zcPxoM82PgB!OXOYoc*dlC846mAURD~J+M5=cJfke6icBzCn*lBHhw2AMwrZ)P%#sNAopDF7U~{MD?96Y9Sq<0? z;t-br_T7G}PH0ER0(WqBKbMldiHD;XUt13IcjR!x_H&pIUo>uHhd}3~Ci(=N?;oon zB+HdRn;yAX7JUNZWL|Hu_(TymmU@vRLbeaL*$R@X4oLHv`&}r)1 z1AEgtTyMUk3+ZEDixZxtVDDizi{BUbQ%E(W1C!MSjXvQnGB^=TA>j!;^=dv*y8Dr} z9(IF7Jv^~k_fA?o?qkQ+OJGDV;)b(u5RDg)1HqfLpa6@{>T9drKb0#mfR^h#Us2;tk zdynRmoXuO!T$hrMoaR}kwCOBkZCs7l2?28J;vB4eA+b&yFW<-a9es!y zj8`nFmg4O+vso&kidZybbusaGMSiwt)hPJJJQ4Tru)XNpS=#h3F(1#V^#>%qcUA5f9)T6c53Q^h^A!Yt%}?V11C2nOjSd*=LiG9sKQ* zbb8zB3CD{DsLYyl#^G2f^BQj*Hyeok`L?M?t^v`+YMB{AYS-FA-9?WsRCSy4ZBFsy z1NHe&yjdo(bAg9EnsxOH&#-Kcn{*J%H1*rl?0%3iKo{*=xbzOvqZ0KZN}L@0*|G>7 zDH`^wFxUpSU*x6mKsiP3oP}SGtSEo2$c)uO$V%&1E9*S|hZ{jkdnkiR{r%0azrQ(8 zkKv3IssO|CyCG4c(zyTd;OZjakgt+r=^lZqAH&Np4m`)Wb}?)`IQq!jCG1u{9D=-; z*qevhu|cV*Cbq@g*gr0IX)fs!lIpu7{)iICl>9(}vAxm}mL4I%Gl(7=dC`uTYo0Z` zlKDJQAo%K!D`}Lo0j!PwPWx}Aa@c*Y4=3UDjX>jAEK}5oGG&jOpGgwO8V6s7ndkER zlhi08U+1*BVm18WhVfY?&Qz+flbc!#C-jFk@S-S+qAsfBSjfC3$UV^GvzV zkg}I7S^S7^gJ4X1oByW`$fZe>mdldWLdiEZAgHU1yv;y%X(drvYSnoZ`63hAW9{WK zTp6?z&zi6|D;9942os;|1>JIIU?Y8eFWWsQQPHZscxnmh&q|?J4vj9owVFkpE$VGox?qP~Hib;5NSPCL1V0esW7;57bFlN9Uzl zxjC<&l(6!Lz;4eawXmD^xT-DubgUh3OaXZ~flMy0wb%+np&b`!AHcuR?*iX`!zPwJM1bXVTKll1n zEA>3@#-JG_1CDKCIZjrJ9uK*_Y;MH0<_U2hHhF5OOCFJLUo^(V1i6!hSKxGAh3ZZp zq?9C&WY%GmK&mN1X`VilinxS~N)GvRbTSYLsj~8MfS>*u0Yx{@dNWXF4D*8MNI}i? z(XOmd_oTWPmSlv4ah^(j)IQPL^Eb7NwcIy(m*$^$cLVK1zn$fc(kV(HDxi(pan<%M zTPMP3=!tB2A#>&-YtCDsu0#l=HM%oDBl<-;(D=wlwY6d*Q+aJ9pVIj_U~D445C5(u z0Ar)gVVAR6!tK7x4R-%5m76zugm|RqW)EeP4+;-W8)sJT+EVeU_IJN)yv^j_UCwDf zFK;uIPgVT@)5Q%98M*RPa^;2 z(w#Lq#j-RK=!h@M0HxU{Oqc#6ZIgTl>+mUf)3Yv5HOCU6HXe{yxer4@E*ls-)Q0J) z%3U8~gJYeYj_>2>8r`uAd&zO3;T@gB88FX9l*j}_r zR;+23{*m6$-S0i?{}qoCB-m3)GUidb6hYHvIo=IyH0sK<$pZ*QLxoIdd68Y`AzQo0 z>^3Eh#RINC?gvhv1(=iNd26sJ(`|Weeo*I0>9p&hkec$GAR{LG@@r|TgHkNT)mUkb z;_T*}W1>x|!Z{jJ1!?8L9ISDfD3bg)ixXb)<%&HNhBgDd9v3D5Uhd3NSRC_yDtr=0 z5urXy$s{1)bwk{lLgx5ui5MROs3Bvg^rJobz^H>aClh6<@Fz4tf5R`rS~MtcYa>00 zY%(LGp9U&;%A@JG@DoFdqI#*#97;%x%o$pZ?w*@r^Gz4)9^Uyd80~B<5ATb~7mh#s za>I!x@*!Yv{&Rl@&~eLBtROAJI@PD}2E8cGQD_(4!;$p-x5J8E&s^^2QoE&m9`woG zIqThKwq#WUr)i7p(rMnNzxt%GS8%xGa+Ire8C!xp_@=5^+0S+9F$8IXj^v+BO))oF zh2PykJ##UIgzu_rRqP*-2prh-a9jULpV;6Vaur;UdJl3R2Q{058K|JtRJI>ITkW#r zgo>8Z#Fxl2xk`Gpn7gAD&2TS7UV&1K9$%>7(|>b*o&3d-A&ArP$p`(GKP?i{;pOA< zM`lV9G)IrBCbS~%S7=T<&hB!Wo)=ri$;)?!m_83CISrONcR#2&N#vv%4l^=32&r7t z7;`b?3{H)rY6yO1g^w!?U78~$D!xzxPo9x07sLzRB#zs-n3PZPFuwxY;mU0PL7gnq zfKj?uSdN~F=hzw5VU3<|Z7izn%{Q`$^S`?)P32kp%AJ(nXzVFFBz8i#>CfvZHHc(S z!VQ@@Hjqn32jh#%BWEy^?zKA|Rtq3vM6ygIcF3tihqqq!E{62`8KXL4t{WEPm!=GB z;htoYD@Z&a327Uo!g@1}$ZjNnI|~y{`rNu&7lL=MfIanDJAQk1;eeu4J-s106>GU6 zFP>I3KP~!Cf84BDmH*?+oFE_T7Io>&7x&O!y{_jNsQ}xH+vOZEJ@fheA?3y^4c;jA z2t=_`)~ED#M)TDvz95@Hi{D3>N^+-l=mC6M0~`GVzZ(&C_XG z&LQP<t<|WjLRIvf zq82Y%mfaf)c!8D&hYjhf>Y~NB@$5R};^DMW+;fJWr#Yiq0{Q&k>JQCM@r#Pxzj8Sk zlPdekKw5L7;o+&tyh=Tr$iY`vIQ=7uZtLmIpWWjPjFA$7Wb(ZG*864`@HNk`SDtS6 zDHPF53_=6I3d)DypHPx%Zn9~lIC5orO5nOJ1v$A(nEfQq@d4qT0l$Ek_;36!wCq3a zI{g@DIq7`mKC+vbK1db0V?wY1KNT6R`P(3CN9Pbq@8=!=D>5GZkufm0Dg2KHYf#Ae z0^w%cG^D#;sD87ZKfh_e)+xgx5%g2v19q1!qQzV5>~J^6ldJl}aO_4Rh)}KDbMcea;=@ z&3v5s7@jr^d*v#%QKO!wP41LB`bsTTVRx9Sow6Jd^1aiQa%;|JG|EIgL9q04enWC- zx-hvR1V``tn7E{aj)ln;C5kRX;8EXF&8bw*Up@Z)DV{hAMTb=uf?q)oKR?taI?)Tx zCUFH0@HzTYH0gvD2l-@gK&KTuhP3br7Xt%|^qsMGvw26r4^<(Gg3~niGbwE*e^qIf z=B&u;O?o>1!uJ6uI#d(MsDEGGe{{=SXLfi-L0ZsrPCdx-o3~Cgz!epmcF3AQhF0hy6T>197Bnz$3;9g561a)0Z=&l>U-Ic(4AZu*IGGi&^=u9Uic+1b(y$>5f4Ch(2K`teP z{d9v)Yd5&;PG`L^P&|Be1XFI54EdV)*j~o*m6Vr0pVJGZLC#Nk87Rljd5!)kSw5ME zi`q!eO6G_FA({fx`%2!(srnK4$ z?(dPCZa*5;rxrZ3Jx%dzp3yjCKOoR5zoKkPdYwqt(jM_kyH6$LL(JpVt}mJXaq$F9 zC!MM)hPj-13<5OUfflBk`Ptel?KSsP#qva(IC9TFOH@2sy zt7#o6Qgz3G_zzB}j@H_Y`eh3Ms?w59=Qx$Ki?4j;&r@pgA*8d!0MHiM5TCf{&Zxs$ zr5Db**rmcEluKut;&4;bExBUH6t1No@|8o?-C#EfSzoXMOO-sq-rO1Mmt5X*E2%Mg z*+QLMQ9lYA_1!RlxNs)kS@FCdB`CfU9vwI@9^_l?9jTy|+jJ6^rxM7@MqmNp@g>(h zPo~LIt@gfcWDt~{B`^7yJ=>hV3Z2!@ha_3REFZQ=N z!{v=$*R)b?B3jky)W6P4MD4i50&tq8N(JM+cd9)@PG`+$od0VUp(o($7nE+F)-ghL z4-qriE4s%=srsGVWy55Ywdy3~Q9tYhS1F2 z@n_|igu2bORmYFZgS*FKs#Js}E5Km;4kM)%D)F3CX;v6yptLSW((#F_+=sc1$W1SZ zC%bfgKXa_w86P>A**>#XV|Vg{B_O*Iu@?6_j0}$Uxc;$Hve;nWuuRX?i^hF&V4#xw zcUIhqwT(u$gw9|Gg@@K=P#`U{x8w6tC_qb!$=jJHXmfR4)ArFq%^8*;-Q1U^9Tj4* z7yQ~ai2DHpWHH|~Q!-s|Fk*6B7ut(or`4R9Z!Oi_%CD{K{bk;6*s+Ca`PO^6SY-BNtIY(NX1F^RuN^` zw{60-ZK67w8i&}5d+baLbZNE9jOmTJAHga-VHMPsp23C+HJ)*~NghGy!b@0aZ#2Q1 zZ(V#Cmd8J9F(J9mEYGEd9#d%Yt=@=Q^oiTiBtU z+_Lsir~mAz@D1zoa_Kirt)Xh+XLsB*^z`k9`BUf7zU2%>>bu%?*!Rd2=?8Y}caztJG~+<%Y@KzY3+ILBzD^`b7xwLD@#W~Q zwI6t1W%o==9j)d#tzm3+@Bo|Un#9><*>^v(=`k%xNFP4Ix6bOLY1nTfckjz~)FtY* z-@Fb!Tf`jeQ5j#Kc<}R!ON`{jQo;VR5Y`)C}Y5iS7zP`Uz5%t%yNvXOBntxD)qPl_ci<38Cc_zbCa_09?qaLkU|de)I~1W$xs-6s|lFHp*u9k#^Xix47i zb6={-3mIMkeWiuTaB`> z21J_~n7-VtPbfvwji#4u20qGVj~)IaN8y&pvnyqqB5vp+g9|KmexOELsldlTxGM$^+53|e;l-NQ@`sSi(R-dl_Q=3v;%B-wmdP9i2LgEjl2X)frQXr12wbt+HrF7f{&XkXy+n2=x$=3Hyqqe3W~$v2c9eRsGu$>Ew0_PfD84nnF19%Hzy>@4RmP#jAW-C+nbDZ2VPn&7Us-<~3K`f0i^1+wq zCo>>Wid^Tu+QLc3wuz%p-qpM6+-DhZcE0hMd7Cq)lVc^;`#3U(X177+N|yrbm6sQA z9nS6wr|r9$W#f#YeWp2_X}-TzE~UhxksbKkc+MkydC7zl=Mw_!C1Q;sbN*)Aw(8|m z|L=A!g8hrEcGxXQ|CfbdZ&H0DsqKQ&%kd4cfI2_IOMa_^T*rA( z5~}i-mE``HdHprToOM|7_oyfKG{xdFl>AXKX=vip zfmw_g{B5&T+s)*b4yyTA2KZS2_B81?O}5PsO1|d%g*CGTWzJzOEO(w%q&+6IV`{L& zK?i=RX}E`Rhh1?Zyad)XDdMrIkEKr&$z&sv!!5;u_}P}g-PeBF?^BqGWGpx_ZAcVz zNa@SWW9vp8VYaK9((VULd$YxkqznK>%q(w*?${-9f3Opv0H-7z?b8f1SP!82#huf? zrIB-zXw2PF$D?&ui==42O`Y>6+nuS~CYB0ML!*B_EWed$z$#L?$*<0$Y^b|+pdO%i zmyM>b6*}x!dL78ICjOM-xz|M7qx76#aQ8rSgk0*wH>SJJV`5|b@JuVjuLwm}Zgo2K z%)jy$>=8WE zNT11D6+NPWUZ%nYkZkskQe_Lg-|U7p!!P=I!d)b(5dR@##F2rnrDr%Zz5c zEs#foU}o$W_N3v+y$)f?80w^49S94B3?)sevLtb@B!=5!Ah|p!j<+}FwV@LU+d=hN zU!Cs1<#EO&k0>g6jIME6X*{EUi%Cstnq5W(r_8T?Q4Fl z4H0iIt6iDT*sPdRY9wZ4EUxqW(+<>(ebTvxn`?4{TqhuX2ZrJK-hVdb&uK1h2 z#MEV(oRMV}{#8}EGVh3cd*-`0Rg@$=#7+BBj&%^z*kKHbB=mGu8nld0@AYjkyiX2h z1*!Nqq{SIj9`3#xAC9A%#LO#X2zHc}temBrgv!QAi|$oyr&;apvQxuQLgxt&r@EnM z)|W+%Xd(WSZ}haKeO@&~tar}`W+PcZKp3lumbOMJK9{Gn#W&o0@%xgJuYwy|VN9#{ zq|pC_Cj-pK=Syd_!HoY~y#4w8Amo@W_*0umxtw-$Xjk<2=QSJ}z`U7{ojP4-K}J7P zE-A~kI`x4vg0C+!Imt8d*m0#=ah6W&LZw_TaLHm@jJ+>l9#!klUgssr1GkQ?*hZ6_v(U^~3!j3IGVK=Ur*znm7^`6J< zPO;Fk%;Rr9c}GuL6DNQ0p7)Bf%I!eSr(NwKENsU^5C^r!5qk}R%QMZhoUJU-944Dz zck{o1NBn#yK0!1}X4HDLMNF4( z?USVu#s@`zy}_g8hP40Ae15-Nzt!8Rp%I%Je+)E?zffN|)}S$C za&U61N;$y~!h6J}rcHl}x9ZiNId86ojb!O2K7lc=sQ;F3abEP7HLW2bk%Cq6_>wW$ z-YuXE(vvHK&ek@%TR<)xb}6Z+iyCO&R5CY(I0oL*7-+1}i{6of+@2TmAd6X9ACE3D z9u3Z(^wx<70je@&8dc96<>HxBfS8i@EnfXl{8&C&eCDvq_6vJberDe8m@KHXmnA65 zKuU~Mx$iB2)A!TK5$UO6ntstiZ3Q{hy*UGgP7qKJA6wtf?W8_d7E&$Z0{6T?3(vlU z2f@&84HM%QaJ#P=C%$Y{qdOy%t-XOkVU%#~V|`{2J$@aWuzy%noz6`_mE8fw^9yx2`6@1>g>TS(87O`fP= z>R&saknPV@ks;u1W^(i()sg#-5lBEOiHiOt!B;EO1f7~Zs#1fS)n3RWEsbq7R^3si zc3es`7TiHPIk{<42lWkGKA0t;R^_;`ck?^pjt38di!@v^{rYHsZS(e+^t-HkM>~Uz zzfSRc#nKmVw(l`5R~j|{SZl_}P@xzziMSsj3rq0YlgLysOYvR@7tc_`sfLO}-i4Ll zxq6A@iHcXH&`u2(OsVE5?pRSl)fmH5HZBi~=P3U8!9@yuPR5V_*R67&Js>r#ul7x8 z^p0NgetLYgTbf)`ja9I46K%I)qmflh_0W2J=7hqFnVS_z!_m>V@jLJG+|gH9+-NAn z6~3(ZX$8(%Wz73ZcXxu-A1lE;qCJVVN8eCx)`^6;sXjM-*^uG3C5~85}u#rB0ltmN-8tWb2&D}6%OXi96r&_ z69luU-JBQq`k)`~3kX=^&Kj4-EHz{q2UBjyW-rtyz z`;e2u8S^Cpp~#nIrvHm$vEiqYg=MX5Rfz7IU;o4T4NH^jQTL8umCML#eXrkQsvVbS zSQr2Q1_GTqZ;T6kwIxwZzV;PJFq5{6hdU>($VWYy_>AWqh*J#RR#?7 z5ZnKuC7hq@5B$jlcz35a$!=?5yQOAq-VEImxv(payNe=T`UshX%-+ z(riDB{6DYXWG;#dCA(m-JZJjlE__|PXXn#Dq0Qs)u=Jt!C2_G|eq_iD)cdvf^i$)n z1^b{WzeEn}f}eBGOaByS%|NaqLjj~Zy7?2#Ez!t=TW%tSHyLa^YsQ#55S&uT}PkuDEQ&$xHV8TF3k_fB$O!h zlt&Z(0Qq^p^`5XbfHD7{jQVvEeAmYLj@pl##nA^E-#^ZPS4sicRbm`Qx65Xwjf_I` zZAi;(1Cgb5LYBVLU8|qx`S7#+!&I3DOxtzYF+j2(G)dZw%xk%=UIXOU6IE z{&FN<7V`bML}!k|H^}GntuzqlI({Qyi17;yj^lzO*8uQ(4G6C)ZJM;;LI6}g6aqlp zpC1~O19|hqeW$DN%0F`cVP>Yc;*$V~zOoxrmG*i6nsGO^ybgMYFl%FVdDMs_-9d4` zi2IFB3T(m1q;^gvnXyHOP=`P7)q4MpxF+x$fiLGrkp&P=k2}d_xZ3Z(^saQt(mBOM zU!Ih6O=A65iPXL2;XnU9{O6(N-ZhdQIIU$1#I(;%DsN)&4IqATDTQP4Z~bS*{dlBoi zhQnEZvABKie|73FVOlBURVAblpC;s+U?eIoaESb`1I5`8faGhO%bI)Jvj{o+x(IR zLNMY5_#u`0GUP^Q(C(WJ9tpLmJqvU~m$p3sJ~%1$`?D4bin=@oU9~Jt1K}I6 z-`I}l3I5tjz_X}y+jt1jNa!HeosC1sGkjal;Fr7b!|1;&8zFo?O|hG!eEjUP2b`zv ztAS?C^j5#$G5=)F`%~xf$p{dU%mq`w{ggN{o_nIfF?A;Lza<+r z++;~KB)t1qn+zB`Uw#=p;W>;hqU!$Yn!<0uPrSAl_{^-jx~vf%MGJUDdJ&u;S0h#n zjJ<(s(fmxV`5iZ@>N~bl*}QZc)hVbIIW1sDc^y0&vD`AJCy?{Za=tw8%?xmL|EPH% z2{n|p%$0<97_MPXa3t#XPnnKl-wKrjiSV@boUpaozh^V>zTr@O?r$Le@4z{UF__)w z#TU-cyhXfP-JS+Sf3FDATCCP(?YPnSEenMrC{KBE9leJ-D@UT{OU@=zV^SJye=ixf) z4{$1U=r@*-cwD1I8^Bp*Es?pP8Vp?EIJK8QYK(+wG^o3md0`~zZ@=^0i3H+?T|iqI zT>3O-v8&lPuz2H3_3{=_xoUrS>$X~N?eAFM7LhI{E23WJg+le!PoX|K$WOV;bcZU) zLDlUOY*P=*V@MMpq^a-a?rU zdkgC?tKTtw+egtzVq|4DHpC3EpAL#Csz5-$k9_!>^ia&cRekIZ&B*zuDxPF4Hpyf$ z@KJ(aS&?H@IiLp77edeG=LOYYtt3g&mf}=mjqgipG-QlxpzkZ7A1d_RmM{1gHRYy_ z4itt#OY;|wll;*E38Qg5o~Wy?Lbj`IPQu{ArA2O8o|YvB~4K0_nY6A6ukFdru(58 znYQ!Kw6Y)eCT0xC_(VF z0IjrsjlHM0+FTUxAc2CExh2ctH`iQD#FV$Al8gGWD>{Fv3T++bPr0_GszwXTmlx8z5<9sDn!>nr8|BXh4I}LxV#c`r;35!u z9=oDSlmu;76J(5iE$$`>uhr$-q$yR zc*)YeiEv8g7+q*TT?Btg8TL~yc;c8W+1GyPZI3oQFg9_PbS#i6nmMo5)|H*bkjsi6 z_$p;;IP4nv5saL)8q^c0y6CAFQ=q<%?o4gZ9dR2rbWUOE+>cI@xLQog755Z)^6X^j zZ%c@sB%O2g5eMtODMhPnNv?~Av^SGSZJRd0f*hT?zJfxinJ_s{1@Ws6Pv&FPqHvNm z-!?j6A5J>5Q~L0vhX?ux!e;nSXx{h(GNqUF0?$w6(A#2X@hV?;24LBo7+qvi`+L&!Qsh{=fG5wg4WJ=rVDt_MJG}36 z)zc5ZLf@zk<&YTC_C8dl6GOP(V2BrfjE-T}8_~$rC)*4+Z9?a~+GoD!e4{mP;B#^q zHx*@eMcCp@Hx&QaP%v;@#7OaGv+%Qbi#9}_p{pJD0)86JjM}rhENE|a;)8&OondOq z!1_}2L2>B|W%R8olruw|LGggmX1vGZef^G%-`4v0%JXSEofnEjY5KbQyZqEX=QOVx zhPa9-8KYcgSgQ3t5??*}{2Yv~c(Osxq+VffR(Vr`|Hb6wI}r7Os@xHF$Gpz6*hvZE zBi#eGMso`!yV%Rg&JvQa8TvU%OjNUO}o*d^kcGugT^jxjbgQL0gSKBtC z{|3rMYS5s0t>7nw^1fSD<5P-@ye6#lGE-Weg&Q5#`2ubPJOdNB`b#%dJ>luhxUqpj z21tYFiK1xbNn!e?_cJ;<7Xw%|vln!l5XTvbbXhIfxbk}EiAQ+$Y^~j@RE%3!H`%z( z?cJv8)W}qmQ!t2!ia!X6$S~MBC@4)}aMs-&%;X>i4^8%X>DS_VlFLlrI%laZ(v1Te zx4zMr^4tFW;gZ}MS?_t3f;Np59traZ^+v`wXVh$}0NUTzp5b~@dzP3++-#i3QcDIv zskP36mb93a&|!#Y|0QjZGix`Y$f-k~C%%C!2hHeoN1ZO$0FV8A%E>!Qj>vt1Qv9fv z?9U|5C`^8+<`6t=1SX=iIQW&`&k?p@b+w0mG&5DhA2g|$C{L<#eq-Zsnu`7zu#^bh z*sc}yoV^mEZ$rqlmtOGpV7;W^0Fp^h5elUV!6KRa<=$j!HJYwEKU>X?-5yt&M*L?< zGftAZp79I5cvikT{??b+!gq1*Zx32*oq{>;80PQy;1j!X@s1ZIL-fSuxD&4|K%uKS zQ?p2{$bvdNoM%;@eW>X$2I%qEGx(H?=$!&SjLlPLz{c{U6&7Y0+_b+jGDnsO$fdcR z_m7mi@LddYZ6Im}VIi2C|bkD=gpg*=CM@BCK(bwYzW;L%n2fTjjHrPaqfCE zg!>&|n^#m~8*xlv0!oyg7F-zidnmnpSj_>r-O`RTChf!)dOIh9wKu0cjgq4S6#cPD zengb!!U&X#Bd-Nmsbbq0D(JrIij zC>Gf8d$OcCn-q>eZamBurB_b?XB%z5BD~kasQC>a0`&$MO?#x5UK?E03(JdoGS)XUeii}gRrj7A5mz{+bdB$j;vd2~N~s+Fv4amvQpmJ4 z{D`@h6IJ0;wc7EloU-;N2v@M4;lO=qL|hzCel8zyC0BMB*pOa?ClqbN-7xC;->t;$CaMg#|l-HmI2~wGOvRvIjwJH z)PAV83VBAcL^BvnR*$EC6Pz3U3($~_bZ$C0U0}$;&YYC56Zwxn?Qe%@9LFm4A;)qG zvrd!`{knfyZ~MtcBA%d7clLPx#*1kp6a8rUCxJod3~}pWg$Q+5Jj66FS|LuTGqEhk z6I0O4k-kl$6gkLpxHF^_RT0|MddtKF!@-*|q!&2IFJ!+Yxtx3f2xGa~;5)jFiTu#- zch~-p3!nhO?LlU+^wBO~!WjFq>Q>5ZF5dGL)j!fRmrd==L?co0FRN`3R zeREiQP+7nr?a1hwyz%>&)h6(ZMYu!X9JX^Ns(~P4YP^EAG&y6w{jMh98$~byH+7C; zG2y?TOCPey4G|SY%7vIyFEa#vAQRDlCLUr2oO7DZQriQZYef^lspf`@*y1dYw@w@M zF^VsR_KMzjDferC^Ju}sDO`rLV9fL6x|Oy#hDUoXdaA)Wl`1+;`%JMf2kyle5wOP9 zhfO?IVQAN7nx7qMSX-c$S;F;4{xkd_ngl{<2{Y=nGgavtJlwt&@CVZQO7EOz66dFl zCYD=lv8}_hhp?qvvVY&PotG6*$e?pIc$dIezV_U>k3Q)T1HRIeLP3P3_O=v;HgYP< zqm#;(JO5V^HRE~ILDQC9^f2U3%NacohXcR1EH)Dg(erct2OPUxvM!oqV^DJt%x4lk z2p*J5@%KU|M|G4njn@rvJsmXBBBWz#SDI>yS;)GsNKU<-#1<)G^fl@#8ClbqAS{yvoiI&b|V8j8KQy)FQ!7NNgpJ@E;ThFG_TZ`yWS)Q7F0c8+8C9fZ^6B0 zSVA%f;bQTK4kA@AiyAU5TC)y35YNxz;W(WmN8B{%bAKM$Y*j7eu6K3D-@>eec0wk$ zkJs-pbs`~}6U>zyPQkB0FVmgg<^MhEha<_ulR3c~9oxUOqjc4TUO6t#u_@0A1U0LL z9&CyP#Aq#J`CujxO?<3|juE+lxYSriEvOWk4^N3qDVwJ~K`NY475rK1-i#H;^M>E38FFTH}2m{aURg6 zF@UAB?kG-T3uxnRGvC4*-nfy>`laRkC=}9706!@&S&2IOb@wX~D7(*Zm#!5Ua5XmWhFxGi^td*&qDqCHB`-s6+aV(DWZA`;*VH&lY`R z`kXgAW>&sguj~DYVbH5!m!?o0B{yWdd5>negTt11N0jD+5D7PjiFh_iT-nh;#{;WHygO#_L`_^E0`lF97h$Cnhh3Z?42cF6=qZFw_-`Gj(8LgxEqD* zHWBJDVXZD=$Ead>tfJI>zFzgn?B-@~g1*yvrJ28bii?&NH73`>sQ}8LK9G;Au5lJ- z)|idSS1eKM+tF;rurv^}Eg&r&f>dM=U|Ycf!SXG%ef*`FIP>Xo1OH&*8*@I6?!J|>tYOQrUh|CPE8oHVkP2lRT zwA0I|$5JMi?oU|E;0HjrJibYZ@vX%1z}Xzl8TIpz@kyakzlR!e z28%ESqdb1RvFg|!8S{1`Pndb@fjJyjWyUONpgfT*WwV*jScxb{_n*JX82rrLG-#lJ zrXJljXh>Nff6p9&pkLwnt6h8wC?EH~Fkul%*3Y-U*WmP_(fGE=ECe zg^42FeRO1PjgWqqt1^&>w^T{MAVOV)_Eh_ZjwvVhj#KbV8+#t?j@t@n1XfTdhh+iB z5{-%qaadwn!^&)&k?_rYL(?kE zkK~wcK*%@@*ykov3GMp3zxLVE>rtHfM#WXotPx^mD(z5M|X?7p}hpn&fUyf%A z5vu7y=LB=mK}kIi`3#1rrI7Zm;aLsl=a+IIPNI42SSK!aIUWarmN40ian36ioyT0{ zJ{CHf5U1K?VI&5vc3?c#E8UiwHI$J_jkxixqG66M8lBT>=5?%@Wr2hVJG0B1NGY2< z`hHjp`HB*gP!gK}CXK1Jt9= z$7eLG1u?ngK9uIYxnIDYMAb!5lMD}V$%{llVa*(ogA*##l`agb6x7<9LJ6@_33shHpTU?3Z&0X@6-vF0{h!j zB+`R)A0FF8Nsw9JhQ1 zB?Z|mYIs$${^{BpV)h<$M>ww0yH0xW08l{r#AZ$ZVzPm%36@HPxp>Br0@cj6eCE3D$A=apcT(Gi!28oGN(es zvpq|o#@!aY%+r+;uY#OqlJOMOA)_JeOhBJ`)UxI!*3H~es9Vl9&{e0kV zZ-p?6aPivt66pJZxkA$NdT&H=W04Ps7<0tZ5HAyPb9t!@lDLT${L>-HCx}gj*M8bd zc8Vr-YMYwIB&}l<+v1(T?(ecP_w?g=A_jm*XhZt`(YHb&&qQx;=F$uK)Eovt9o|YrZaM*3IkVjhWoJ!cOjwNO-@x_k9 zr4h=47SgU3&H5(%nB@qE<~$w^@_E8Px71lGc=;JLtTAOcB{*z&Y4m4{-$^BmTJ5-) z92p|l-Qq1QGN#pq;eHtC9E!Z9wUU_QpmypZWl(Z#YhIyc;gra)&Dw~pu?m+cPS76` zs#{`?UZxW~u<5Yw$?H&`E+1!7Girt`HmQYh^5xqXE}}ppqBVw(w0~^y>R`RKc0#Vs zUY~uX>rr%(^xNc-gU-k4?r=0`nq$yUTG%63NPY*3FffEEYhMmAu0IKr~5#BikkG$+G4rAyKR0^J=&arS(e0(eqR-fj? z(9ax`cQnyQjmT+Nxb0f`ypPZ>i9DX5-pkE^VBx3q@%&l%7J+fS8x(19|9yWloB<;Hu0Rl1;OR96BdOWQR*aMuf?Iq z(kI&RBp#t0v8|Z`*bhEIW<{nmME$P{+u0?hz`# zG=)C1(2k(S_hut^3ffZX54Er#Oe>iUO7 zXJozU)cvR+%E(h+RWCt8+fvxm#{Fu!Wq!9m-QPWjB za&y#g_(Azz#?dPzW=?T=SYvl;@7Ovh4!YDB_K%P2>oW#0;fBeueIL)HEAuf;OUjR6 zN~L2-wNz%rk~Ugi0<8t&N;(6a-QRs^tpn>H?_%G_Ap5_yr1P*CQ!xm$Js1#XaEX1M z>*4jBU3614o8r-pfB0`at)RS-d`Afvz<<}(CVeAen2?_5L52>7Rk%~3K2FSf4pdb` zC8>tOaPJ!@vhUMSymrzJQdrDo>nBT{iSVFx`+=}2F*jfn$cu9yi#VoTe;%h{;359c zts4k)U3ahN)27Ni8ECMQHf_~RfT5HH_96EWgAjNq-{-YFzsoRmHl}fnBX}>i2WY|* z)}>)Di7(^%*L3OAYxyt@Mx8;Edkb6+E%j59m96H1;5>S{i3;fK+uaw;3R~{LWvo^= zH?vu1mTz*LDij{q9`m-*y>jlEr+Xq-8ID9q{n7GLVMVP*vQvA-Y_Yqku&J9B4E14D ztbBVJx~TCcN#5>XRnsxN2bpP09Yn~r-zQsE;U8Hu8T1Y2Y6NP#+vnpX!(mFwS?Epx9yU_sEntz zJd8;_8TPg29qAcfPH)b*`}W<4(q5zySso=i_j|nec;ke&ByHviqXSjHMJ_%gBxSym zf)eeay)R8E{=zFOs!SCJIwHKhnfYPQhuE*5OLJxSCL|-01;P$GAxwL(<*@nIqWIXD zcL=bj&umv@JpLzo3T`VM=A;>Xeq+g zon3N~T(6=_`>wB00$k2DCsJ+&n`LWyeU-2Aw$Uv{}h=UTzz zwyyb?Mg*bgLtHMjKQjIy;fzirEeP831Y*xecrG=-vf0>K`mTg+5C7&N*aUQg4n>=% z5`@fue)j!scj?LsEM zr5jfiF3k!vx?LI35{Jj@`bpeUV2fh1XQH2CwQ$s78A#NpzRll5rzN08Yy?YLIhu;_ z>TC%h+P+{-n^sJXuXvhNQ@$ufGr0!bIv$@wBE2v~a8X_TQ|frntEr|i*?xO$m!p_I z7E9m8D&hums{l;TM}18eTw`+UNO=k8YJx5RY`$N?GNb(}!&S0B=V#JOcg>(5;L+On z_X{TO3SvAF<0&u#9`$U4){QKw6n|qfl-ly9K&qBOhlxqsDajmuhC}&g^X|?0Wt8-> znsd+v!;K*|zn(ZbEJJYeQ%&n0md)Kq?uk+rNp#Y}dyfp4Q85hkY~{Y>AW3dgsWC>vg4c@WR z^?{-^#c^Qf0>k3NB^WUu$zGsgdoIL_LYDa;BdQxEuz`^YzqRxEK3q~8k zz(To!FC%+VQW1N-9JqlL$0KsRlVImJrdFcY%?~jUi-8Wrcl~_RBIOErht*C6N zFtm%{MxUXwXf9EB{*^MarWoXp77s(y^gH#*jXj!Aecwg)xECLfkreB%#`A>5g@3Di z_g>;HWzlre8|iZn@zJ!NF?7EMg+ZJV*~#w98(y4M3>ZR?^UEzJHAHN}9Gdu&z!@p_ z3rePfe{Fzjj)+{$r1Qw=bzhT6-;#af$QtrBVcgm?+{(j{A`9#olb`tDICovxq!P7h$gHu|K6 z`%LPS#%ud3u-@63g$GJ%2hq{=X&B`~(w=zxUzngN@6tpgl@4CrYrX^Bzfw$I%ud{QVmSM z*E>u!r+9c}S%dr|zF?fG(m9SBHYOpUO%Ox`U9cslZ;?%ucVvNR>5(BKltWZ&Q_$Af zDb(uL7s*iraqyOYwc5+d9Jq+9i3l(%r=ZGLXf}kw3M^f&P;le1;+q@)*fW0c97wW? zOclvN)9-r-QsOFNW!n;m8I<6^)Rj%;TEuJMLTU@FlKVQ!EyZ8waC!<*E01wTbt0+; zxxs24q?7H9X9Unn4uLW~?RYt@^x+)XBaP|XI1cfL1-0t&M$vSPKAZzst*@6nrYxH@ z4DqZ(YDNj3v~)hrip8R;^vQRNGL2qCl|pnv%=%wEeXr-&mtNWdl-_;j3J^^AHcRLJ zgWeux;)pqW2Yhr-VztPwOaIZYXi{E_R3b`IEY84f@}3Ni!AqJcA}*@9q$Vxv+!3SB zmsdedtn4D>B4f*b2B-eZc~PbBb!vB#L=1XK*Nr~7aCNcv@%R;?od(j(c~NX&b}%pV z`9h;!k@9FQy=4K&bI-1|PxZM?u1ab*cqF zE~qXrxhY)7Weo3a{LE4C|N8pfS&^U%0#qzLPF32zPN*Jr#ydK9J~~t3n^1dPmhT`& z0>>w;sxa*2TF z_*A*j^E2e_V;u99ycbXvj*hnibUZr!5cJe(KSf&Bk%|*$!T<+SSpVsbqn`tpGLNG>AK0ktf+O$ z9ZNraHBr4)CbjAcdAw_}$h9v|A6Qbu9X=N`xhrx+SeRCxmlS7u93X~#9P}b{8b9;< zad!Gq{mvr+t;{TQ;`lKC3|1JCKgmgSDGM42B#TYE$A|UjEJ{!Rirg^9g9Ck|BFIVB z<%q6p+6(gM3JJl_?2k%$THd3$=84dXEH6s8Ai^SLGtqhF{M9-U(pk7iVyA=3lOXFx ze0CVdxbFUA6kB-x1MO(O#Dq(OkSFqmacI^Bq584*k$bfw*v4c>@=y1WT7B+TM_hb( zL+iBHnG@a7d^CLUIVe2E5CAc2a@tdjBqj$k$S#CMgyu@H@108MR&E8J#!H>=pYy)=0Y`Nv~nW{Ou1VYQH?7yIk_shX!O;9wZZG;cafTd8bdz_ ziR-Rekm3sYdG?BdFL2_*!=#5BQB}Rka)8(t0vhV-&mvpU^J=*E85ILxrcKRj$B7}z zMC;Z^h0B}(@VgNsXQ!vKT&z;(FqAFd zErVPWbQl0BIi4qsT+hj%wO1uRP~(B&{BC>3);Esp70 zR-qbJpAijdpf@fjBx+E;m=p2~#>;(=j=i>p)PEhiib6!_EO4&!56O;M>Aw1&L5T@{kMMQGGs8VS98 z;mqp5N8>C;O3unCL5=V3%^{Ha|Hwue-(gUIf~*OC4;M<4-aIhTy_t>@aY{)kLvqH+ zsk8oIxH0I_V(BBaQvng}1_=}UF6hbE-~H?)>XR`X8kgWWSt@|z)qKdwq?E}ax%B#B zP&bbQU)ej5&=%jBFf|i}okWT7g$E@P#3EV{hrQ4F`$S{!W#z}^KF&_#vV5c)+?o@4 ziq2(eiy2BUKmqtA`Bha_L(2t*7$*tU`glSPa4u4H%#;%Pt7Umctp<+uFC}-&(8!d%s(k$eIUEE4-m&V*#6H z*Rc)aAwIUsj>^KgYc;jb{rtkF9j`pB^In=9OWdsgcKu;vv7-4IZ0=3*Z%vUR!-@587Hohvb!oN>zNUjAPLunK8 z3m+SY!R=(&1^S_NR*{eEp4RNXg{ZscdECs#EPr*q?iL{Y(7_%8HavE^$SO3NL^2E@ zcle4Cpb_JhOC|-A?iq1xz=qd-z%CC;SV~~s3;R{T*ShPm-id<$(aaFrN)BMSGF4b?-gI*FN7MTjXWtVmfj~W@$YMW$Z}kMv?9eQwhY3R|K^=?cAyWrwcN+7dde2eD=uw!RNEcoKuYkkSP=DPU4Sg6O?b^nXl!fr(!&p{_UV4p59tc1% z1UUD>vFXQTFc^~x=F2ouh-vp6-{xtfcIP`4>rSZT6sJ`~RBQJL=e&cy5%VGli&3T} zp3XgL*Xi4fmY%qincX&M*@9fwiO*ysNMc*>RB8e?9v;xPwJL|T7(^^_6_A-uG8)Q6 z9LAz@A>n9H)6F-HxsLH5#GZII1j!`oYexhBbePHH}wr}BlxMA5AAS$vVW5NjPf53#LURFLSLu^J8zr;XNm!Zli5I$c`% zM!^A0W`9_}*6<>hT{S*>aQR~!2>C#O=|XwQEjFjCjfC{B_zvmoWfN`W9n6(dC;cX|Ka`B&M8+8Zue?*14|$)*r!uCB zWzHFwFgRa5{1#nlcI)ck*BmEb;7noT4>=8CIH>B59Uv_~oHKjL{f5g0SG{Z)>%edE zS?efvnuT>u6OwaSf|O0=lv?Cm@alD_ILqwqJ|`J`^HjfTRUc~L(?3JRKgef6zIT}F zvrK@^A>4BJJxjpaZO8iHpn0*2MpyZ82fgy~;*t?5v@HA2CHI}~H6=dz!4?NB0Iyi- z+kB*pOHw*FayP$+G!2?RJIfznBE0|Hv15AH6&!?)&r&fn;MJ z4iLV#N~NH3>ptzu+2cGNy!# z1=SbpnPpyrIXl0K;|o3iVE`(~0S?1~9Go|jxJ;fb$Gn%nq6&DM+176V5+v}8h`i$J z$jmDsnM&s=EQKm?rjKJZx;Pnx0_Q&EFML5bmC1*^;tI%zvTVTSVfxwO4N6D>+f@}f zJA<=kw~24j_{o%vhX6v`2_sLIWOX7fOF~)?XhfnfwsSf`YZMlHVT|^?#Vnu>N(_sZ z^5NO3O-YT zPena&Zr*M9g&X^RzX2)3t*2i)1U9sOC8efbjrx-I6}PfN4Ze6=@kg2vNxUV;eSt<2 z?tS-G5^VQEL__Hq8l-W3eA2)m;F5;+!IeY&C?-Z9hE=PR991-O5Kj)fF{9)yH!%#eBN&I$VEtIc9gsj=hZ$nxu zd`pk;9yaQ*ZoBUJzGpk>yjR?C;>FTC0y1m8)WxrfeHM|6xd%kP(IA5*y-GI=7kWEv zQdpGs{?H0HsZTWwm;VUaP2aBkra*(l5B7i@lhVAK{;#;2^BO%8$lf_ z{KYqXppirO);y#~&+i@w}^BfwXG1H?oNWVHpul23`lbtp#?uD$WuJj!DwR z`C>zVMDQ(0J?i0*)tz<8vMefMx28v)L~tKhd1EuU><0f1>2IowxEezi9To zSq4$3c}9vDm9!uI>5XZTo>bhz#=1KC(X}yy{VWcmt!YHBbp)Qn5zhQ1t~yd_w}N!& z?dw5~{R__ngX%#$e6w-Y9j}vjb$J4=q)7;b;j)6~as~sFQJ}qNft=jsr?&GPG>~7aC}2!&xGy6$M^d8m+cfp zRdUIhp38nN^p!XYyg7^cem6UV9_zzJ&@DC{UM8uP2?oAq`g`waDr*+#?3gs)E7GOZ zcYs*k-`~gd(1_7=8|j(T_7gedF`pCbl$8L$s{h)B#k43%{B_)K0Ch*ycJF;1r{(z^p1hj9)h5Nj@d218K$`)bH|2SPp8-64kUe4?ZOq^1LwquW&7G_}VlLvo zF>p(nHVsB-0P8BHm@G$ewUL6uoh2gPWw5xB-D*zRWjx}HQYLF#(DV2~6x~@3=;33D zOCjH~XDB_%RcULBy7z`7(CXGkSOS>Mp}7kD`tzDFt z<>P>!GuUL8Ki#lqn&IGc!k9}|{5FGe2B}L{GZ|;MgPTSqaE%yl;B=ASa*`SrP?}|u zvFT3{YRejV1~aKtN$c(gj>>l63a6pGB^{`8~G!aRG`FD(rYl3hrz^nPAmty z?tH3g0kPy{RE_Wu)Oqsu_}E~W#_NAViD%LZpYo+IZj(_BCZ5MsRVD9$iw>LaRlP;VI8f3Cz zG|{{-zRJ&WC1U(i-nyFoFDu8}v3z=dbM8jxKp&#)9is-BFFB|&%<(1YTvI+fn7+s; zM9UlDQ<|h`pJVfp=gweU^?KDO-&a#(PRRrYC0)8L?a4Cvhk~!=1|G>s=ReyXxz0`% zb0?;3E>uj#_%AC)p%B7K87aZ%O~kBSxjDx;?^kN3Y_i%Lbu9TzOe!iT#$SUQASm^o z^=rnK(sATZrWf2bd@dj&@(~UgxWUP8oSZNrDwPX$U8iU?9;?1Fp7l;W4pCQCN~;vW zC>RjFVy;kWddH8|2U#QEh0oXels9K0kd? zp#2|nCriofkm33*(-#uAtW%EIWi#vT9|zSkGJ*JT{PsI)*n~RQCh7MBuaV)Bq1QDz(3y^qHw-k^TYUJ3wqC3VKfk6jEVNZ6^8&l7;`(bd)~ahzu* z-1C`G9cYQ z?3hQ@!T4B{6&eZ@3Y?R)NnU+UP=cp6uL`wzn7a?c zLbGdzSUNCu+%7iKPWp%Ps%na->udfOWSAJyBO_NxmlN=-w7Yw4AZC~n!DgQU*(igNDq>L_tXrNAXVdrC=?uN=sh(oL^5 zZkyQh#oJYn$6!TC?aC0LWHAXu%^k&F=(--iEv|V#Vl8^UhqH^aeKDMCa|zS6X5*Mc zQrmv(-@jksz2eoB>XkSGj3gC*`XQMWG#ulv@^%Ac+IEMS(7F9HQQp4CGJ+P=6fIZD znZFeLX1-oz726d2W)2%;ofRh+fu}KF-4*8u6;|>04bUy8Vx=J;)!{<+>81{SKiB>- z54ihiBD-K9uutr02X=Kf`S6?kl*jL{Y0eT3_l2?ak)>L*3}aH)}H%-H4o^`A~!6kSK0#BW=We}7f@v|}|y zWD(t7G)m`_-uAwoxG2CC_pFLtzI^PFswqTLACv)!2Pc)CT>3>#$V*>d|?}ZQKL9uwP)bfHkPfq%-Lu989H=> z2TAC*jbRd$268*ynBiCFl>Y=QNH?M-dJz#Y9rl!(D*^)k!eC5FRc}DT7g_F*Q{>46P|@@fhM(s-zfg=phRl|1;Ri4&P`?!(i8){Y)XiI$ zN}&6UN?|C&S4)C#QUBQY+j(-mp6ky<)|o6wC@j@p4`09>WAqPmnkhIec@ad6uOOkV zG_Uv&ZqnX+`r(`Pa%YZ*AV&jsP~wS8g7TpRPjX*731Qu@-9UUH+(zc+>Elvnmk||^ z8AKx%zAAG%RXandW6L8Eo40m^v?Y93q$##Uzji0U&}Tx#q0>;nYhyyzF4S%32ANem zjDBP>Ey!XuVb}NEhI5RBEz~h!Ax*1Jt*yrN)e0{aOP5!BIi1eIk*O(&j;AA2M0rC$ zvVZq)qA(;*T)WR_C*DOwnXChfTokcK%{8jA8EU4hv=ob4wl&bmN0fU%<&L*}N=PW@ zKfI7h))C&P`RZ#S^U@vn-goZsquTS40n;;0P|bb1ys7*cd7vIijJoWuvfHoxja?Ei zuG|m`cV>HJ-pJ<|36v^qt-WA#vd#1NNciZZ0In0ICPO)gi1DB#{pw$a)m2UU4nZZ4 zOsofZaJj8{1!IA4kwH#|3d(2ZGer}-NTx?PQwmUV%lRSbLjWn>J;13}_&DNdlSz(`q$~<=97aH%F-kk&XI1FdpvlI!_#q*KwQBkHiC^Nok}sKZXwx6`P+W zw_FMHlgT6NDz|lWXp?k)PK{lkEd3g>4BD`o*?V~K%h89MWtgd&BXpSoYO5j zN>>+UaTxfD;6d`BN(!x`uY0o1FL#Z($axdmoh{Mn)PK}dQgsZAM92QY-esu2;Iyce zVT{Gd8l@P?AN_V2YdJ9sd*f={L-c&5gnH_pSsUzt@h3zy8L<24UnlcB_Q;HXFuEU< zem?m47F`5Td)T;aQh#AB-DJqq!}uy*C2haMb5+Rv>Xo%zccq=`**{A@z40YC6_HsI zBtNT{?z`f)K+)yMo5xuUiE$sM85WrFJWe7LDak4Swvk z7P4e`djIT?V-1qS#j>-wk8WJp>e_OjOkgaS`jf@GU1^B}{h6_FhMLZwk8}{2b{(XJr#q$#5x~2W< zS?r8kEJUN&YWL5cS07X|D7tQ%d%Y@2L(!}p_YdLh>37qJ*O9*y$Tn@(3N+xq{|oew zk93uLARyQm@#nWw`El9*eiE-ncf|L-cZQ~o{I5ryHu}GR=UG~j)zUQs=vej z?|c8dWB&J^Zs!ExabV-dz+#QlX8G@r|L6g)(rzwUQ2O5=o!;qVeW~_aw%9=km3lxq zByDx7t*ZPK42)2@)@Aw^Fx2Zf!d8m}3ZLBCG0>#Y{!7?lGd|`heG`5HpErQYhm17e zpd2*^4_j^it(iu|kNEXFAECezwYVmc)Va-;ZGDK{Rx|C6=y*h2Z<=SC0PggtedD~} zZtr-+t9KM)6g?=e>u9>`Ddgtn!_^&n9Q5Il|Nrgs|9xR68wSfOcaE@RPluhic)(%r z#AhdOzT;>PJ9;!T+E~{G$eWxq51pEb^Nu{6?oIIyTf!#*s;c#xjmOMiPEW@3j@jDU z3Pj*}BAmt5TL;pfcWpe97dSrNI*Cv{+-R8gK0`4@4!?DoPJ7u!(3j`eri`bt@z@89 z`v8vIFgT7Y#AVqa&PN zfR@V1_l=D+DlMpA+;$jjHyM1q0G=AXc`DK33GsvcX|UCnHj@WjHu2Fca=hAr1Jk*P zXw=r7xt+ql`7(FH+X)s3%6I&!*hJhaZvsj>;VhX<)} z*`UTZ#j|p0kPR^lyl;`{1&h%87n=?*z`gAsUKu@^d?ArE~ zN)_pjpZ3>CQNm+Wx4if2A2BaJqPVe6lJg`dFHtcgoS!J8rCExd6j{AMCJzWX27+~H z-kHB*JtTmTzj6<^DfGuD9&g#)_M@YElbm);NMi%sEWLX7P9;y|<3YhkHfN)HF9ki) zVUbFr%w7%G>*|94JG%YPG!~?st+Xp*KLW~R+}a=flzruOcrq0Q&M_Vx9@7)YOl`fg~FCzpy?8*hUepQ&0tv_t{o<~|*OFy42 ztL#ROJgfFkFN;P$O@>b%A9kIzlO&v*^OEAHG}Fi`kvl4zKOHr4U8#QpyQbkd>g7X# zq3DfaY#@!D+8@mpdS($$9Z7{Ewk0F|h+zm@UoFx0Cl*f0fp^BEn_Xh7lb@Co9sagW zwmCadzgQu+7i5bP`I~Jb$-N?8AxEd;Kuvj6CCH9a?^Ey4X_Qi;!nn-+Fu*?SG1(E& z_3^5ye#Y{lgaA~V8%jU<{TvoOqJ6Yxt}^Yr{!vIhlK3IER!9e4>{AzdFlZi8Z(p9X zb8!5(E3!eO3GFa5@^DioVDQBs9zeRMX(H{`#!+)?&<&>~($jnlTZ{sS zIrZWfp0gl*wc5Ol(zL#fRygc^YFZjx(Ny`OaxLc@>V3Aulxw_{J@3gAtM(~%t(%g} z6CH?tZb8kRFOXxPU)!9A9LT@!8VWW3vGIcTup$GXAghZe&0!NubON&K!%M{?TmM>w z{&$9`em;-A7xn4zw_e!>spqS=kwG&{T)`;&w_ADSjD7TA*tafJ)=l*)3 zt9vZRix%#mH)`VIPZVORqch)ryV7Bs0z?n@COUKHV$}yP)gr1q?LAw5$$IDhNm=m( z+TBv@R?+yIe1X4!10Dgjh-&4Wp`;tTbh(cfe&zbW{`=IZxaxxMESTC2{_H!ja_|EX zZmQ#Yk#ncEgBU9h>oEGsCCx0RAMME%Xe)Jxt9S`bi@xb)7>no`^AE6T+mjE##+b1ZnTh~WhCh}6p(FAC_kPQi5axx;rX@D+0ie1(i1S?HNV zQhwA3e{M(=JzAg*=Uw;_1Zeng{9?XOab4%WLf}Qf7~CkmtjZfXn{`50QM_F+Q`$ec zMMJIMcQ+{uwFA<}obNzxEW$p`);)5Z6(0N=qsgV^5CKU6O|8GfhFkFFb|E{}hpsbI zAP4Sf2g@hRFjKp?F}**iNHg zb52X$b-kTqu`Bg%W5t8ro~g3z<-Kb~cw0D^2{22KrsNRz=Iw}Aw$S%#UWPk4(A3B6 zO3Xa^xr?`*kEFY{rK>8U&5Fr@=u?~c0aDD!_v1@mmBFrPox5cN&k9^_Wt7}%n!b#( zB=Va3o{a|qidj`Hb-lZbJY6mJX+J-m6IQU8c{k2ww-xcXz1dcCP1m;soa-IfKK5W0`^)z3llA8$ggTLB7tcwRZ z8n|kLuG+O3Lxv){#TI=X03~Wqrl=Cehp3r$)%qchJoBb)Z0V>;sH^ z-o3GD=apXK0t@#gY>js3v;SNf?DkIAJjSFF^AhQd`>$`Qig&V`F#T#Vdc!62_T5Zs zme6JW=eCmKYwRxiv5@|8BVF}9StQ)NL2tFA;L8T9lpv=pv)f=5>GO==CmkJx;43Io zhfvJ?+C}PbLl~}mB<^9$J-*3IK_{OpNy)h|$pB8=dYPG&{yJ4ae^vd^3p6q@a;kJ)4 zjVvCZPs)Sg+~+t&xLG*m6K`tb``J|I{)9}nHzD!on&3U0_S~18%!FGq)p8hl&{_6- z3kwY-&MRJs9|T)pV;-O9S?gT)svkiTgZ1wzu!f`Gy0DGDU8=4bmTPpm24AsjRD!=< z35l$0NY32(ikupbmFmFdMqBhY+q<-{azi=S_;}c0l74xMUlBY+hJy#+*6Gyk$J6bf(SKVn|Azv!7BXwae)@J-*>Ff$A`bh|Mgy zi+B1v#g;8^<~MRkEK4s^adds%WATsC8T6+`Xfhu^v&^=q!ypUGWVbGD$*uPwv|<+)=9XZ7z#{i2=vJMG z`cdP5ZU+}W#dp79xj*=MnneAa@E0z-dH%7~J*5y4`vGaWwd~A*?Y9ao*0sn}v8f!RP+#;l+K3x6DUqQTLiJ5Jw z6wJa{vBdS4(|csHfsFmq__(5?XU^lC;tZp8Yr( zGEDARbWps_R{y@$z&X`iIUSvp@BVeEDML2p(18!JN-|WYluEXjAUVIJ2BL=>ck|Ha zS7rTv9XzG@3DgQbxYozpJ_ImRS@Jh}V-z=h(w;q67N~EKcEhK5lk=YJCRGQ;|CySa zYSR!ct6uB>7i921Np%2&vVqfA#w*+Gt(uNc{(ewS$P@Y&hD(tDMM^GWcyNjE_5N?K zd(~N*w|xpK&~_uCSp?&U;(yuoH6jvgt|dtETQHhZzE6I9FS=OTq!=a7G*Wls$9~$k5D~-@^;{m@~PL! zwf+Uuw+9#a=4Td%_lfPf1$Z0URvQ*=^1z|plBGkes^$@iW#64D(^pHcUOU}3{Sf{0 zkkYz++tKw-Kqh??NeTHC?5x}6+6;YY8>X&bGU~;$%0a1T+&{=))hx&Lxv|{I+9!9m zEbw_Wz5ft41Qni0oJ+PpTwk(9hL_g0U*(*vo1PjCt+x%@!P2#p7;S^$@R{bwKmQpO zty!av#DlVt~Xh# z2^elJ*J|Ix&$eYp)mM_o{@Te1Jfe5Cn0fzHf3>)7eBEGouxxDmkyS=YW$MAALabzG z!?T=ssFV%|E)QgZwMbjQp?0|K_l5avJ3piL?K$Mt_LTia#xj;E1!nCs zM)|W28KL`g4`ez7oDw&aA=a!WpX;< z6u6K@6}HU!1%5ppq;}~LLhkt6) zub6aZBcT95ee_e<^3;BRD4lvbynz{+VsRiwyEP}OTpK*8bl0lDDdb@_LgT*i9M(Ro zw+`|th&>B;h50qCUF$ES4k%J)Zmo=AH=fD(Z+Z(hd6i#{RVk?R?wYJ1TM8EXeUUPa ztOEy4Z@O9~8c+7>pbm;aS|2vGr_d5zXu2-nPh>F*uYartN@xvPgd<*ItP{h+q>i!A z6|r-cQ=0j!fHd4v1%2TsdIBPa2kLdmZquS*8D za&9kh>cLeCcG^qfDq;hk4J8js9_B@3ydSE&t4w&M-YDI-2;0ovq)K^kxxaFR_!eI# zrZmBI{nm>Y@6cYnYZi6o;{JrH>s2#;H9vt;MY{@dTz?D%!6j%Y5+2absH~sm|H^wQ zT~zb(TdJ_QFsGu?!0e)Dwkf^^rc~CWbFm65B>81zTCu8SJi+}o-m|#dJo2(ZG>h^*Hptz6wew5?wQo`MgkcUROYZo0HRb zZW2?romZ7e@l{ejCglk!HKmtuNnq;ogoR?`6uBOFDjhWlq4_%RcVOv(=%fCqr?)NuZLTk>({RMD49JRL*_I(FStlH)mh+F16%wB)nw$&{n6w< z3i_`UD1-s&{l?nVJF0rC`A?>px#IFYrcG6;%nwtKGC%x`HvXrCw~_w+*j+mcVQ1K$On!U%!@V%r&9=LUWtB^eVBSbz{YiA?foj_uf?dC3k~lY= z4$2qY+<`4Hwf^E~=exc>QULqF9j@*j2EQI#vja=6Li3D?3e4Df(%wYHxbeeM_zu%d z`z1(UQ!V;_wLNmsV}bavL}tbLyG{gtLM6iRHTwyyL3<8}PY7%o8gL(h4T#7rl%ZbE zZJ6|?3{kq1&!y}=2*z$`9gOXN(tjiMmb}pHKyuso)S%9d8+#8ig+y8etUo&n|F2dL zJKasMX0E#{>**-|=DR=GF{?jVHEpLg`M7+~$UNlRo4{e^C1=SI={vKZ*5PK3H=sP# z&HshK{_C=ql}>tgv3-c9b1Q^lnGr1eqkAE_9$xUsG|{bf{Lt>gY)6>d&&mb2D+p)e7u|{W~+lxPkhtRf!sfvO}H! z&!ekvoH7Tug$i>2e=q1mzT|T-pQ;*GM|c(ZpF7k4A!xE*cE2gqE%=)$@Ssp{FZat8 zz3?q=ONbSo`zQ6LfmYYRVTw=tCw${sVGW!lMD~VNJYCbqS4UXi<~NHljT(giL4_*D zOs`)}dD^?P`m%lFT)$VxlM2k`$<=cIF=t048TN(kz$#28+)8K?*lZ?wn+(RL{nn%W z?iVRvL?VTKKU`bgn@vo{kg|Qb6Tmy<_bQ*S)$fsOxrQsdl(7zXk=^^(mWCTv_XMl= zrQprD9~y(bZcy(%sx?N>=FM>J1<%F5X44}%*$W+KkzaR((k37xZKqN8?f2i_1|uBv zeL3^``IGMLsqWwA25*!%MAX*2{&T>L>1(6*ojd3yssrV z-wXEgd3(DJlS5c-3!Qm+jEzh-Q^83;P2qAfc@k zcFHq!m2b4Pm?%5?SP2M{r^_KnYcak;osLwrBdiP6ajWvvZrPkbOe)D$ri;s@|5QYJ z+FRVJv!!tURr3zpt=0^(b|RZ^w-JZSdrk04#?HcCBePL{tP4^U;7i;!RgU z_p@@vsh7xkhAvOdn`Fxh4(g_db6Od|uBOZ6y4JuO zD>es$^0yjm!Z(S1lG?s4fS`Atoduh!x6fqcZmEdS94R&M+N$8+rLj|dGoHgt)j8~U z?TJZ0vr33%tr5FQz1DNoA)wd(m}CE6a23D^>E`*sMRVwy=KHmPNSo)#v<=x7g2>3;8&ZHZjUrJdwI_$|w=X;?>Z$zz^%M6sgj^A759lnlV&NO%UJN$c`WKp>N+7e41av+!{ ztIoW&UjI9Yc4AbwQ^~Vc?tSTSnryO;hWu=Zo1XmxpLU_&!Q9UQq}^M4o4bxS@uw~Z zfHUe%Uk~5oSotu#6KxX2y~Pm(Sc+U2+qA2E`2VwCgev& z9B?m-Y@m)YTT&CFI%(8p^ufnL(Pds{B!p55;jadOepy-Jf~1Swee-{je;b1y3#)Dq zh{~69grnz9pWJ)uN!fJEa}Q1s zt$oE^<(4Bhe{%e{nWoRDQd7+<3&&iDc@F<%T{)K5aXK%+qd^#@xN@z{$GyONHVd&N z3+CD_apiv*?f6Tk0zfJ@C^mOCZmN=PPdVlJkET;5qSx4w@(s@x*rI2Qvh@g1V};4TC&!oc2A3@x{UL%zLLdLoP9;<6?gkfi>9R<5@(<#{&XxhzwrIE zWdL>HTjlhr37uU(6XE{nRZa@acdgL<&y2mN?5Xe^@Fkkjg;Kb_V#mWqM*)D%o&4PQ z^HQcNXwWno0T#YvsV`g~hVdf)=B9F)b5muMyH>TfTJ5?X z1Zi>B)J?Opz$5l??kb^kzwJ<41ow8Blak%Aw}w`~6phStDQ4WeFpL?JPFGIPq5o(o zKiaEYpo`0Y4q(gJ`T>F5H#}A{_pM(~K@JN-{A)UNVMnK=RoD^`1O0b8rEfG9VB~gj zYVFSD6T~5i?CvpG1Nf_6mspkzh0<^sh?cZs3i8%%}^w(=_lW_jt{BzkZOu z&qigjm3`Ey@2$7=&i`TRt>c<}1HSDa-5}jvD&5@zBBdfF%_w1X!x-HqAkwX%(jeUp z(x9+Ohipo1#2hf-o%iQ{pXd2^`|KLKxZ=Fd^LrfM>;>Pw;5+me5+~~3ap`NA79P%6 zs#-g*z}CKi9F%5{fvHv8Q6{W(3n4_JLCT%X&boPQ$m4#wDP&2HovaGpdZotQqc#RU zd6J(^K}i#!T@}(W09gC6kbTjWzRWJ08Hw3jMJmW-XyXNO5oR!IUNW)@+dt*%Xw5I( zb?)G-@^7~%qF*^biZE69Vp#tSP<7Z|v=s`dpY_5qog8_#C#Nd(a@KO-LVw`*nS#kNn3zG;&6&>@SsEsj~=xsqrXUH;Ra-%V88;7mQAIoGnx z>wiLqr)^O!?NLN>V?9bn)xagu8YN=-suN*_^BE~CwwCAUO2)oV^bKw5+UDzh%Oxk@ z|4_*OY7i%LziaR>IsAHckgYHJCbXhQO?bNx>*(|d*dX}2EZh0m>$ZFB0e1G6K8g=> z#}xJB=g~ey!dd=2M$)x|bmFbSzQ9Y`8}w#yZK2njGgpG?XQ`eM3F-YjV}Ba0Nb#R0 z%sgUh;3Kii#wN;Yn7+_UC|l&duqZ0WE^e;La^P3}Tvt59HYRj#gz0f~yd;UMFuf5y z=P1K1j#8s%OG9FBPW4j)zk6pry;{8tK)wO1ueGzehPVF}Z0!9=QL|j=cq#gVWIiRN zBM^jbn(^v*1;Q4Jcv}p*&kJ*{g%%ADg_JhOlv!=qyIl4Y@?bjoM^f_bwe5mV_|$2qfOBwh|t$=SIWLuHmpvh6To7kD<>JC`NBv7Q6(T>*Oej^p_Lx!h5 zDdp#!;20zA`M?^nI2>D9e@&?7%m`;OSH4N#LZ=`21M6km$HYcCdYM|sTT==`1W%A% zqpD&3d+ev3Mxs4YKtcxKFl9o#CXzk^lfnsNaM|UzqE?f!*yT)rH1qhrsBdd>F70&g zzdH|yc&Zs>s%72_F6SNzoGjTGu#Je8%)kZ-GeYi&sCyCE2o+)hH* zpk6vXbN=5RpA8-p@N-uEB;9eN!Z!6~Cs`G48%=E9%oM4xwK;Q_>=5A4|6q0J@hR=v z#B8WUn(6|h$!xQa{^GRx?{toZUyK)7&A+E*4$@2uF{7Pl)a|L1%;?-M>btWJ2(#5W zFyv^aSJD}Ven*u&28?D;jraSyzqBJKt24JfxC+5v`(&8)5d6oZpJr=AQr}#L%9VHg zy%xd&3@H8LZ~U=mGCW8<^T5$BiF!VK^ISm8P*l)0hbWuf>u913lVY4rYvw0>evF*p z+&Ykm*iT11u$xS(4n6vpjInz4fNK4XrqS8TRao|C?0t~8sh`Ix<_C*(-xge|rFc?G zBHNpA9I23F<*%{~fc9_h_qf& z6t_i%;B)?_U(BRJ!tjFaKI~Z0n+<%}0j|Za!0RpH^UhnzyJinH4knugUay44N0N}z zcf1|ykx(e)y2{S${czlU(91V^?!sp2!&S33ZHpZM$W0_LUemDA5`h$&_QxfJEPPDd zh6`tCxaL<&u}WD(#-LCc+IZK`+h2CD-oMu1(@h4x?N2*ayNPnr1ok*uZyyN5ZE<)8 zyHzm%5;Ih1LeE%?DNCl7;1l0v;dLO)Z*aV3&Dp zB;x@VfsEJ;4Jz5VWl?Ho-91%IZ!%H!qN4p}()+kYUyWCGy%zOkE);15cZ&p|Z7=eR z?L(>S3+JWAoRS_o1TI-*3V`XYXReOxc!HA3a?b8MwM9lYg9my`2Egkbyv7T1sd=7% zVJJ2ZWRkA6=+)~uG1?zxn5i;pC>-ZKx-z^c2D>5;)F*{6JCWl7hI9c+Ax-KZ(Q$f( zAwF1svn|F%lfI<#7BZpQPXDF(MNq?kKI!0W%BeUBl}NXj-x1@9qmNFZ(wp0E{ES|D ziuU7qNTO4F0aS9hmr#JgUgj=kkK;gq#yrN#Q2_N3$&Fc}zyD#ko8pZr|4a;U^>Oex zk|uC>#9>p8wUC%*T0=g5pLIwe7NT*k-7zTOQf^EbjJJ-J9=gR(1f_LRQ;fCXKlc?R zJhPk13U&ITzLB(IR0>#blCaa88GY!VKfwl}F3V$DlG$jbl@+jbO$9+W3=X240VcVr zxA(v7?wpHikzAt9p(u}<-`ISN`QDJs0hwyFtKxXo;TRbi4#u-UhtNNvneP*jgyAAi zwIs{%2!svIY^tVOdGXSf(a|J-E>!?8<&=JBaM^L@*l8rCyo*&;%Np#_p?1DAJmO9C zpO3sdxAJ``i>?y9dn30Pqt3!!kmC@;Q34S;Gid7K4}ey(E2>hNpE+@K-`}2Yi6`(L zEry-tl&ym-fl)-j{a1VOF^J}p#{H+~s$~<6*KfD1A4NLki5n~Dn*S4X{!idb$H;E1 zjHzf$)z<*%fBer2*-ku&f@B>!;Ftt8+Be%rQ7PJ=*D`!ERXs%tpH?vf7UjaFj9aC` zvizBgD1R^6P#y`U8}#=)p?)OVYJ<$HzdJd-JK@^mZmfpum^bTgqOs4cR;9Ex;q8FU zbu}w|heNl?Sx8*ySPW-Ba#HZ$Jh2Zz=%PZcr!k1NlrdDsd&$9V$`B@_N=OtWmMPf~ z`rf#+RBXAvcO^Xag-2`e+&aFMzX>KGO$8Rim3i?>6CI=)?@$$oCO&cvr!}2{0E<>H)eE-zUWz2`Q zw0au-pLa*|cXb5jN@7in->rq7D-udg4d@lC(g$VR=)ElTSYVJx`pqf8zWxx)A2v~6 z#y6~WYsh6%${Vf_wc`MYIavpcl%2*l7w7WCb;c`S`nPJ zbF!cUx-c^~2ahvexx0GO?)P((V}u)XP7Z@7v@;iSL*L6SCJO)BHpP9GFleB8DF3h* z-??S5LbgBFyl0+91C;Judn*@y8YXFnor4z%={Zhdix~i5Vvd{iP;cmqz2ya_b~zEb z*7BUUUa}|X-qFmy7O5a%iFf1=zaets3GRl$B~>HR6Ud#;53+{q&x>Ebh%bWW8Sba( zv}`LFa+2UFDgT?+&ogN@$tg)#Zrh#he+kot>6Ilb0qy0ZWt)$v9M(F~fa>}hO)#6UESh$ZqG(%H-L6^)p9|istb)>5bIH* ztA#HFbZgqNr}^hId*gqdABmqq*`_iaxPuTBC?_zVT4dYPeWk~{LT41`Yy)i%`P##S zW4Qt#x$TpL9sMJD8H2^q%8%(^YMM zMrY^cpQ5EK>2(4g3yRCyjEraG<~N&=8JJc(Z?5ggW_~N0B$K)>7m`@2FG!&d($>BH zshvU5%1H27c;C0#zw7P%1tHa)aGp3(>YH6IR>(8Jbh6gLEnaJUaC~NH8F@fD(cPde z{3X!&{=hktp=$HVc^D0(kstWzl##WnSYYN2CJ>CD9hVcz1hax8$6GEd#_xDJPt=-}S zG8m2UX8M-7&vWso6*Lmc`uv16COTf>jw?8uclfQH;#@zv-Qwq3MeHE`%W}zQ$((p? z*CR{{+3LcN-sW3vFxD|Ost=;^@gh?!qbH6XY-K1#3LJ{lUcOzrkWnDzy5{>tB5qd2 zuiyV-F{dI~ z6Cs!Icbq}EUj3$aO5l3j$d8|CFLuW3w@!qm^eEKiL&0 zmwZ}U+JI}HTgYaqjD)}AXW2QW-Gc+A))aQiZ8#eUdKC5L+v# z7^EBT7&(z=y(_!Po5!zUahkFH-^Lf3`h+67DsSpKk20y_axo8s}V zu96Oy^Qe9Zfd5QnOb(gzg$G2>PntG@3Vh5j2dFkG1io zizT0~IV_)}ec0Lq$o<2j7>mT^9txntFPf=Kgm$rdJF&8IEBO@aCVc}y&Sf8Zkh0_D z12ZV!I4M#4R3sfyz$j`4Cx~*E(PyN}iDTT6Md?Fu5Nm|_&*@$m5z_M9;S3Kcm1)Ja z%I4@WsZ6Pc5^8@{q zW{E_D=F)nMsx6XIP6A)_XbV$(r3$KP+LBvC`faYKdi(R1a-y0N+k738jLveM#W#NU zH}N!T&Xw>0iAtNIqI*;($BASJa(FWBQgQ-}+R=Ow?hPGGc#)x;;c+6YRo;qFVU&!f z|6PIAFAI26xPpVs>gv`z?A@WpV>)jAX#o_Re2fZ42oc6yz8;%!thRV5Vjv)gOJ>nbx_O z-E*I!+F#w+xZH)tYVWYChp1fUJQx(r-LyyW7;xZ|mT2o6qjaQ1PzDFl-#-@c8t+neaRv_jFp5Y4|M-^jsbu+>sB9-Ly__hA z?9o-T!l|dj!k@NP(wE7=EKs_$81ac*w4d1+t}4MeU(Ijt+^JBZp<;6+j`>iOL0eYx z$m4x!zzEd6>yY5#^#iALyzr214^IXJMi2hmdwu(~@F-~?!0v7^$X z(vWP_$1(t>JidPR_s+id{*{SaR(gH22yH2$w-7W&u-G9l!IhRy#Vw}lkl;C{<)ia! zCtqy+#(2kH@nZM6H^0@4`#*b3Ol;$MS#25a}k_ms^1e~aWJKNo&yNi@_0BaTJ?Q|*>Lm(eTEw_sIU_#zT=>fIr8 z2J>=&qI8qy~F5r;mg?R?^%i<6jm@z&ndZDH%wHv9cz2#smeMX zzPFck`xHo@bD)Vqnosn?V2;cK-&Z-6`v}#__N;bt?M5^Vo)cSs_erl-MB4{U*+xB;e%`pIe%~(_Z1-&5NFY!eP2FSY zp^FK4!VzXzaqM4ck*VfiXB|Da%UFzw?$j~7CuoPvlBK& zzQ11^+0Q|w)*iON4_~KyZ3DpZjVvPy7r$uz+S;J&pVeh*M%p{H$fAY#a&{27z4+6d ziP#$qTQ&|qb?zXv??t|ZK~BjbzAz4t5<^AvWbNRb#$uN64!6-@CvwOVUr}S6&*kNi z35)B4PkuHTV%LR-^6ZMZ^SuJ{KB6R#bs2o{CC8dIjc~nNmJx^Vt$NCQ5(E_#_?U|s z7u12R~;I*4KSgMu;gQhk`0-9-2gQzOCb|8_ifu1CHMPLnX5#u!9oOsULWGt^TXPtuNI;^W`LIL#gdK`ellEyWB?Gbo1;z9 z&Sw6pY#o9xPmGW+JgYL4PD76UO~d8|(Fr`$))src!$S=eTQy4Oud$@?TpsB_ScP<6QfNtJIF|12dIEfq1BZ4Ci}FwGb6CH3|8!c z7=%d7StQ!Ck|nVJ(n~ZKX{*d+e*(*`NlQgM06S7mr#uBXFA=HBy}8R?B5#BQU%Ro z?g+Xr^H+NYCk5u8ijI)uI|kh`#x(XygQe$9!-ZIiRXfzoc|w`Y1z?H)dgAx9CIl&2 z{s@k3#$lcwhtZw;vToB;t~yvRqcHZ^;_ZI$mf+FFzsE>3WH09-(Z;d6(@3xrnGGPD zo)X;{^o2^f`e**?XI1m@X8ZT6-(mqn(IQt7W1qpG3H+U6^Zw5Z;-PH$8!k?`5Vdo| z&PN}z1HL!QhG#a21xO5y!VC`tT)|ud<>^F`fEz@Ciuv8|4C^_|G{{q3pK@=>CXxuD z`fqC8yY$w|hXPr3U;7&KlRX4>M_cX3UDE}VCyz&c7R)6b&*I~?|0BZ%8Q};( zdNP2U`do7EL*?J(8t-)?90-Y$V+cCCc!xf}i4it!F;7^fdWhe`@TzxC`12{R2Er(MeVPe94p>v%{PsOO_$7QVjZsm0@F_mp1-{D>gs)#Pu?1NF*}Ec_JjW zWU+|g)vBdW$3w+-sm@m24uVt_KhE{`_peicsTXc&{s!dh8zNk?{Yl9Lp~7vbbj5CCD$qItBGmDqu3jz4yu@s zwggwW1+t29E}Gwc^4^ar88gyWD4uRHrkDQ0K>FDV+tv*6EAe8Yw8 zIJEDg6e$k`fOB@NKtnJt|Ie5>uBi51MhZ#d;qf@Uh`A9!Y461w;h~2Z(q6Ae>_k|a znxUuy&^upMa%a+b$pRpY3IOs;wk%3O(=^)ok8eqPqSA=$edZ~MMA*lwuB)O0QC=0;6S_;Q8pTsGE{&R_m9lej%)d6EJwsnSth zg!b_Pu8Io*fs{)ovp<8BMYW`jg>&fbU#Z*8J^SvkvXjjlHcCZT)6>ljj|Pc}5ZzLb zK)J|le2@um$*_M3A-#?2V<G<9|@y^cxn!uGFgYL%)Pf&V1V@*(Wdx2)~Y@ z?O&OGV%Bk!9C1z~;bJUswVZtm&a^O0qCng3+XHkp=(2%=7N#`D1)^oiV zYN)oGFGE*+;7#mIHk>-Y!%7jF17;k{Mzq$r9^%^tlG$0&=|esp z3>9KDLC;!sZ2go5I9b^2j^I+seq9tY6qB1AAi~I%S0>xD+R?Lw?D->VS4wrB2Hnfg zu=zwDDaC_UImN1;W0oV=7EP#6r`(|x#+RU_fQsO#xZ+F)IqbDLa-QBikd*|?YiHW$1 zE{;ALx3l~xB4LzU;RQQera##O)#Xpbjx%X1KT8+QpZAPSAJadQTy#PNG1K!GDtQ#v zinp9k`w$3!-FRl-S5FS|<55zQxf*^98hc@1t{zzXZktD<``aj#_P=cn9chJ9djhrI zoQekOegZvn=VO{@DWs-KF+L{BBbI)NMWGx%)O71X@RWA0_J~5jJj1lG=U0$brKhLs ztX(x--qE?N@Rw$K1;N1^$iJqJ96W6a*ngHXEGDO5gwg4@vivr+j3bAf^PK}qoFJq z8#Gei)%q2EoHeu^3(Vb|ac&fk$xFxWS|f}`J5XFDfzi*-{=-1Jce8JYlS-0?94WfP zJr?jnl~e!v6<$J_)pB7s=qN^igh2Zl)!A6k7->kTCvS!UMhC$2evqD$6xis{{>@$p z{_V$^*w#`=6KB8}J8Ey`jdJHpLDep)ZzaN@I4?+`ql-=1JgnzYvA2|)9fd4+V#_0Q z(uhSK3_|!cC;jLsv9}=%N!>gz6$%lziuF&Ib1L@;)5<*k!V2}<{M-BIsqp0a_bVW= zXVB2xq^9>DX1{Jt$XbC#R3uoV%!!w=>*@(8yIafeDl6> zh7s?3D1Iv`y3hh(*a)qH*e`U2JktFckPDcN zFTwPAlook%HPeH0{n0rgxMcnmsyu4hxn+aRKl|kIID@LdLy7(ljN#>0%`ZzCSRt7* zvdf`XAPf{nj0rbw5+Hjwy;Ds{bkQY)GwMYMIFLT@U}1Xh)$=O}x6maXP@VMrl?2ah zflDPNNiv?KPyFM(JPY{*@3kCh`K13IVQZKc_Qc68?S}?Rn6$tA=g6K;8b-r70W&y| zKn12lL)kRnHhGx*9@LXMVxWv(eUK#mH{<}}r6yIQ__16!2xrslvpunpyi(aR3U96n zvF3@`zaRyb@xGImmjctFn<}ZI8SQ9FaVK3<@Qc9<9=Ww@qA$AnwVq$L-O#9V5QGi2 z89$j5OpHpGBd8Q3uxE{FV3$o^D|9;hB^eM$g+tuv)7*f6&`K=+J-;@wXlB`$I;KPnZrbP3;8;1N9+D^LG-kzS)QKr% znP%svkt;u~4tWPh3)tBohV0(V%|2kXmhnl=+;C@d+l40kj}f&ngUG;Mw->X!EG)iH zK$<2RpEOLBo2i1_GOE+oo8|iV-}A-iF;$0TTR_Eof5$Zr6;dq4YnY8CC;Vg?$}>!1 z#!$%4oldx@>e`O+ge6mm?|Gy5{OMbttxtjGZnNZKd~13%inleDq_$Yn z^{;f$YfT9S0k9G_JQ;ifZjR>h-qi*+G2A)EbFQI7Wna+@<8WAs8jLLjBq-M8%qTQP3Oa0vTyw=E#q5ABoDtuKm^?JXx+<0U(^36JA zip;TYd`aqvEzfm707Z<~`KG^4jI%|Ts{howz4l;Ou6Liio#H+)u`TFi9SSsG#+p81 z4J0T-Mt5&iZ3BbW4{9(fSvce^<5KTUR;ZXU~Uo|b)CO8Qfd|P+dcg90wkcI8#l1pbPd!98NGDAs? zfJbXQc33BCVrucl2j7@)6OM6rq4*+)3WB}6a#VA_Ba}xT#paBtbFCT-<^!4U81pAT z+9MF9yU!iT3}IGwT(k&j_m%f-$ZDT}z@pNHm;x@i!*=xCq}bo*tl7_97u>yBUGXTP2st=yj| zbCZycqM=vB!pf^V@|G%lnO;!3I%wC#IGcy`RcQ<^1Msc)v&YCHv`dh{&Nzy0n})h{ zWhjdMRau8Bhm2-Z?mIZHr(&W)$#&xX_$J~vSume6uk2vLZ|VV35|kFW---Qvay|oF z>jYMYxH!H8ghVl0mc*{w6I%Hn-z}xDf<#-A6^7CVl3km0mkXV~u}RPJbFzV!MO08) z)MRR`)F}dx@6oa>J^+CDx!TMml~6u19^Ax#Dnz-!O$An&EZt#JQlGxD9Z6S32ND*m z&{6pymBx0zT-}z7`3)7%yT6|u8~K9Qz{0Qi0zKH=FxBCJrxe89E{NJ@1kX`WI`P(a z$(Zfwv)Oz5_=tj`DIqTQUVGYXrg5G^nybbV(Z0OBqnxTkM2H;C&7Z(*n`UE) zvSKtB08>!5H_-(W3Dq3YEkk3`%mSi1_(r(fq9uYyj8)v2+z8}DX^RGl^Yz?2BvBUA znr!R6$NDX=%hI6S2noJm6gqTV}WPdAY>k#MRARt|T)Fl|Pa?g=90ZGO?k z0n8sqf?L-8%YKVo3lh2W`sr~9fBz`Ea_G2F+Hdvrhg5EVF!Kjy?C7?uK|y;_o#mpy zohOXDl;Ma{i|Am`_74eGG0aQ0IrUUgINboSpu{5xD)G8crTp*~zFFv`WruC48w%R- z@KhyS{Uf*Ipj_5LjG15OX7HvLnX-zJfmJzWZu)3Gdw#hcmG%fHL4$v>=IKFk8mEY~ ziLz+rEZo|37c)v@jUUbDro3$$g)IRAEa1k;499jA>z=zba5MSHYYg6p*f~Q!>n#-g zY~#-RkEeVhYe6v8iffWcaHxhuF45X)qA~P<_~}7b&N3+NUqUg8l`(;Z~8wElO28)_vBg!6CWrNyg(PW;I7u^gWvImho{|)hVbKNMM zZ`^6e8V+i6=@ey5{YuI7UvzMFbMZX1pv{;wU3YS0=@aA1QGo5ETjsMRdi+$^Cx1|B zu$1*pxA4Q74ME2ALqBw)Ow>t+`D zBWdox3KUNj456%@Bd}@kwUq$=wpG)E*Lj9cHZ92xLqgcmhRc0JX~mQ|U|u25dKcS* z5`N8eXdUKK<`m1gejV!ey76pI2dNrw83H-7TnC(D%!*4{fP^UYkwl6p}oB!bp2H1h?2t2j!dLeQOLnxv`<>& zUY<3^jE$NVqS!Tan7HS(7kd@5=8&cI7wiqfdg!J)s;bdYxkK-*HT(!#;`~a8ix(ZOo*5OhU3@j3 zl`g9$vCWHD_^Jb)F7begm>mCq-XByi=V`j`dwF?;yuYHvQF;>e{ix*!`^WPVuGpRE zLG^7=Y?KhG>e}JCX@lw&dDdFd#Q*bId#(juahC5KS@(!luX|({en&uOpi3I|bM{-047FC($kt3*kA z)wOsbaK^fkffdMoOUP!iupt}<~hG<hQ(NT*u4a$rxW&Su)1`;j=&O>U zl*YfkF1jIbPnb3S`w$I|^s=qf<%c&Bnp=e@a)b`QAyC({4RFqi@0QO|Fg4?C#y*XH zgK~Vt^;DUOu}UBciye3e#S2+i+?K+=#W55|sVeVH*b2Tk*qL^n37lEqPu)bZ#d^cX z&HJ|g{XBt_1*yWmdglh$iQ?8tSZlSyL6~9tqwZ~A;r86F&8-|@ZjG%hjHq?8o!n(7 z4+#Go&f;Q`GKd5i@cwkolNWpT(6f)`ot^o_nmmHRlT~ZMg1Jj5o+W-w&$=$7M@!An<53>QpPx#gBx_ts; z0OM?6-M53wcgIiKokO{b{J#KG;V5O{Y>}2%-7C#HnU=I67|W!=cIi)pc}JMJ5>CSE zPORMS<~XtW)+d0S9G0ktanN4}Z@<<+?s49CGWUzke~Ql=-pL!lINxa4Owci4sT1SZ z)aH@f%n=E98>iro*~_Yd?7NQs%X$|&FmZveQ~6C5=j|tYO8B({9E;|-&T4>`odN}P z^kfeYCcpz&&C$u7?~S{UlM)sddc70f5ZG%Lklpr8Ab+qHducPhd2tSY$b%V_?AnQn zl>D1tag%C?sifE4Z(m!o|AbC8|Aml<%6kxwi)Vz4fJ88Sz#_7<& z>re0I6n*;dC^0L)RHLD6#d>>cdL&|l(QQVz4mXg3-Hi2>1hLXdvn%=;Fd~-oD`!WI z9QMgmvdStjTJ&~gH8XsK)EL;>A?1(!TeO9#zU`D|kNmA~=Y9e2JhK<&u%}nLxtfnu zedD^4vWb~&Uj=95mD#T1RQ=NddQw_qwq@>quPCZe0cWUslyt!DcvAhA$3raWU;YiB z2BRYuviyZyj?#ovj4X-^U1$T zFsv$45=om}(V^E}9_bG!*@9<(o-jBXj2*{~ES=TvfZ^C3MFSChhyS>+1pnKti!TG8 z@9UAdji8SzaP_o{;`=^SIaL5{YPFjuwj7eNz26|6)9k3Dn@}lkxd!V4bSdgzG^g#z zm057=0mCfW`u_Hv-g@@~X8&b0XN_XSA`xm>NC4GLuL_w<7A%zyKMmhfy1`;<5?Bjb zzw8uvjg~`Qb;w)HJKlMizRB7=cGC)nNszdMPP$C z>=<*Xy$+|W$O5;}8XfIbc!)ZuO@4upS&kv7m1Jq6)jq-7Vyynk@m=c4TmgAUph z;457L8pcSmK=C_=g*E0|B^ObvE9_akiug5TL_74A%Jg@(g$Bq`PB)hSpa!`Y*!s$OVU^P%Jco^}Iuab^!O08x3_Wa^s z{8r?Ferej-KWA1|c*&u)Yp@UOY4OQ#dm>=a3=8bXqYoYR!(E0E=>CKSNi;rR| z{=X_51>~1$`)^;xITm{1fBvVV>$w^e>g(I3Z*=RHzgW;J63tLSLB^?i54UIj;eM|x z)fw-dXo>9~PP#Tp{0-&d;^{L3c%_MS=&F{{DXrC zjFey55F1?ZJ>u{g#1Sd)`)%m$j#CW8t>8uzXTXVp|46{1Th%`=GTqI&7G|x_399q@ zFFsPVLZEA#sP=Keji>vB-3aG3nzu4nW38o^b=5@eCPd&Vfl0foZ{}XwcUbX$~vuY4XlcD)N{v2e?9mw{V*S5man#&4taynk{+L!uN|N&%q_Z~3Q@SiT2Vu2sxO z8Z6!3h1yG_eDKB!6aP$z%B))h02R5Gy=*z19 zk2~9HnfBh|5DU5%M8LH;-Ma--0|&5Lzps_g7M~i8;aZ2S3q19@ z67h}eYq7!NBW^;YP^D04hfs)k&yRYqA1OU7#^yF!%CULlnYWu*hFUeRZ($WHQtNN8 z9)4Fcl1~vnY3Soj`(w^*N9%>iz&~xM$5%>4csYX!Bz-p*#DTzyp~Rr14-d~mQ7vM+ zg=)=dnct{Lr;nKm2~oTmNV)WA^mxJ~3+VTgk&8G%<%%&gHMI+@0n$&PVNE(W)t!Te z^Qu}vtFQc_dH5&R{WwTwWxh?a9q{;lN3Zkw)xS&fv0w&V-hZk#imqy3&!eSc*+{AW zO1aNTwEJLJ11Y2nb)q);hd#FZ3xORk{(-FHsXvKVe4gV^Z=AU%Fg;$$da;hgoaGJl zj!j^#n_&ejg;u^WZmQ6y(-Og*BkU|x2SyIn5~Gdl@pi#XlN%Rydwe!Y1CF2AM1v!* zm&;03uvhA0b`WC7upLez1=6Wo`I_|$j}xuF_HI!;l{s)Q%?QeTJ3>x*R)1Yb*=!_I zL;V48xq5>|O{?v~YGX6D(BpElH$1E}_D_vh^t|*=%(k{!UCtJzN|9+B%HMnS7z0x5 zT9Sp1X4rxlx>qF3XNM1dCmXg%p|;l*;k%tzhNU7TzsAjmVmE+r&qfu2b5?r^ncc&iT!8PZO@-PITT>#*dgIHWM+Nq zz2mc7wE6W)2ZL*KR{QHa6NqqN#aA>OJ7oB=`^U!DVxH>`)lwi-y*nYHkPQN|yI`BB zQP`VXyTkYvM9j)H2PhI-Upvo%jY7(B@~v5Ip1pVRm}^xqk>Dlk-c{-wTY&=F0+p+S zVrMV082v#w(9Y;fk=u^5vY5{H`d^@M>0fW%hxxRjZo5o{`vN2;y&rPoin@{>0_h=a zTF7w+0$-!iw};l+AEVv~#yRq{P^51JjpuZw3G@V$o3%K8DlF#foVlFYp6#fLCi3tx z(~kY`U00gno=5+GnyN!jtN5NW7myK2PkD)Q9Hmu|(I!9iM+zabbh$kY{T&zp^Vajw zdTz2VWR%?D)6IfZqI|t4F+E-A<{Uh!bIH}KAp>HK25ii6xDnTyHbt;);(U@5Q=?L zS8;p3P;$mS#$od|zLbwTo<96jK&)7({h+|qXRd12-UPek_rM?$C>KWyU{dT$iGpAd z!;Lm0)s^SQ0zaNjQyw2jI>QznoIh`7Ow3BW3|z(9yB~NkK8GMDI&5M;6{L0+Rm7y$ zXBFLt)~f>mG7;iGx5wy+*C@mHSqKdD4&h661pbFBNOv94_8ONv`Hoo3k0>Aj_~ z&G$E1T}?E)(Dq@YifcroTJP1Xfbjvk$$!Xok+FrC2P&X1!-(OxaBo3}Tb$0Xjlv_d zoXSDx2`gqxJc_RP`ExPwL1fokD(taf;ydpPCgVjTUBMzQ&JE}2N(_$xQVfYJ`bJ(V zZ|hqT`YAPb6vTX2D#a)Oe6gvA8O~AIr7V@qBX{JqqUA(<=91C%^)8LBzQ+pET9Tx& zbGSaA19l#C?y8t(2(J%6>*{?7<@U^jD~3)rHXzjsnGKNL)304C*B;?NH+1ZOt9wX z`t!)4iUUKG`u!QVWp6@3+pJ8Cn9^Pz=;>xGcZpI>Ce>Ul^$eue9~#@OmG1GX+fVMc z$b&Yq{(O%^iYd$RwDK;HvScq8G&hu)D6{BGIrE*+Db>zGO}WX_f2TG`D$jdK?uH#8 zgMSxF;@al_6Zj35k4sE|E{isigq4evI)}U)@#%%75Pu}$U=cAAToeaB%X|BfIU)dG zMkgaflg-d*{HX#bud8|tg)Nf^n2#dkxRYyKzHa(AT^3wiHJyye{w>>>_4qtD!pSB& zCU$pzjuXrzai@?UCAt75@XKgh7>ZS=O}yy8Ut~b@KUX?*qP9m2!WZf739PW|;gRZ-5VA9|z=@w^)78vaV|<>?J&v~8lDOvKwcWwBLsw|I9xPX! zD|{Gz5=2#m7jn))K6w!7KlL~S%b@uqc_9h{QB|ple9Evz5zCiMgE*ej*=T;+kOdLe z=RR+vDQACtXci1}5_H?8r9#xoPK`r|pxYVm6+#dLxOj~ShOd)Rc4)WQ|`)$Ak~%f!33>r%lLy+n^7)NABA7bBumDU6|;3J6A4M8$JwUi5=l z2#(!^m~Crs9{x36)N8!yd<0>#lk`l>65y(MSV{kqM_ zFKX`~i=N_eL9NJ-l9g`0*Lch40RYOYc<@#vKWpP&yp@p6KD{x8Sr`%(ismnpFS8K! z3bC^mpOENXAhKPoG?#DI^VpsMeH}HHzvfK1;CVmtljW$09Jr_)@as9c^~D4fuN#<3 zSx=@fKQ_0|1^c$KTX^Y^X6L^cg{LZ=3p5kuVg)ZcE5Wy!8;@pK#CchZ;uuvIxQRBg zy?%@I9Cg&82yY75kO4cH)Y+m6$HHv2qRO(bA4BMZJynzM`Z(L&qG|2U3)LL}Fs>gl zNx2=D2T|-B#2y1us%4hA@txL2X3xZw>oA48LB{Dg~InH-c+mT($s5%+YO;M7cE9?Jh~=iMvro zkI3UR9?dZI>;G*a7B?cTe0A<9Klk^ib5I_dx$Zx2QN@bG$WsaTo^q@fdBeqPZ2zfS zxzN)Pw3`10WgRsgnn_wQM-fw!j><#ZP7-G9CiRV73x zUTQH?-38a?mAD%T!4n$(KeEm|9?G}x``ODjW#3h{BwO}vD4`N63E7ztW8WEsjO?Kp z`&RM`W0~yh*oG`)$(YH$WXZ@3(vamjyYA<@pZmG)|9HI)W9FRmIFIk~JwBiJ$7xLi zWRj=n?Prx7_i{OWw~L(%7X*%QSaXxH<=jIH&5`V7<~dscFudT5G~bKWF`)I8H*3AM z$nEL*@%tDhCRfQ}sosI5vEEy!@Z(LDP$t%Be%PQs$TK85_uMP-XYDEDO5D!WsB>`r ze11!^m*47mYJ`95gy`KjA_rX!(Yyt;o{hHW%jK9lIZ=34p;@7|JqY!i-{X4h(*6qE z9dUwz7OjGkb!k}Z-6gHinOY|Ultbh69qyD|{`O)f1-cJ2`b*;Y+&JW~3YoS*{tE?k z3wSR%zu6G)QUin*1r73StUq5sdiQxT9&~1)jA%A{cJ-}JiOQbl)!eg@fSDBR!i6IU zJmZ%ZUKf&a(SA)?CNvS+o{!!US82W{wXd|XKFlu0herE^)@H6=1q*0(axO>@q1ine zSQPwNj$4$b)kq~Olwei3%)CSC*rFHGwdMP@_!Q06ZiN=gZf9Aq^cl(GxrL#o57X>l z_H|(W&R>*%XIfX5K6cv}t;_a24%si{J8)$rB5$A{_FyT02j(ILx2Kl6m#OL5EUv&T zbVk9bXuatR15$kHwOtY!`vc%tySKxL9_yp|sK0y3sxjZ(f`0X_%KKKHK3iE~qvtyF^;19TAD*j|ZxGN(^xBo= zXHa%OggLtXG6Ij-rK47_)^&f;#2j7?f9(RF-uR?6+h3t3hlK%cA2(0v*KNcgW(jTG z#_vWQ(&zCWe0qub6ZG_>IS-0KS+;R1R06RyXzhRaITRzvAr~W&r^pUrk6}LkiCE|2 zad%YG+7LB#TL#PUsUkDnElIJoPU_Xr$U>V^1kK zq{!XVzY_e!rMwm9-|yp|u5s!r!@J~r_Gh7!@Xs^dNuG!pOCocpFVoz19wz&l$TON# zB3gYdVarpuzWCzsm~yHn&`VnXqPvd{923i0SzD%=AMNWr>YHQpq~emW=MWhCr5?(@ zGJetTx{dO^2!C?XV7lo~fPk)Ig?hS?PG^G%>Hn8M8b@-=980z-HsrPqb9V7Z$5G_@ zQkGO&iuiA;{|3g}wd#lj4ao*~8Sdz=P09{`%~;Ff&K1qAcifU=@0oM^*?!%MIE>o+ ziV9{IUVYFO`YLV_NV|{)&lbOua9^e>@O}=GE2Rl)R4vnobfS|^S^UZN881PD7XXa8 zuZAt2;abA7;1b@d~mQ5d`fPGf0z!Ix-FBRu6 zqqbPwR3@R9t0Qu`i}@@;-49Q5;=VdStw80D(X5>;)nSd~>CXH)+}KPdq6@?^^mKOL zT82~cD^JTG*c(_n#wFhVy6$LCPt#y5oCDDu(-@}2I_4&48~)~#(Q8~B#fh=`Z?CBp zYK`TF1eaytv(LSl)QkSww47GGd*$A2OSGJ&Xo`K=WhWK6(lCCB^rT-K7LG5oUwQU0 zI@|W3VRdNRrR(FauZXzaD}$WQ!E*?1Q?|Qn&kQatUZ-AwFs*)20nS)Pubp+%gn1*^ z2uEvmKEZe%b`3=z$dsy~`e=|)8)@*VeU-%>h6qyq3{Sm#uJj*TCqXdEBr(srMo-a9G{Lz~F=6p6PJ zK{XlsOFOdx@NPb=T*IM07Dq_A(&$QIJ{T!%yh-Zg?v0>h==^%_i?pZ7)4T-GzSKdD zhsB;!%f&1x8_B0>N(wo&D<+hX56*ocPN(^7xq&(#i@#PJ6#Fh&b8GUEATc9#LU#E3 zSLQ%j@x*CdhC*fi=$l-Cr?C8qLD2QM??y)sqR+PP(jki%qkc0LT-hsM3XEo!4Q7|> zH93r8K9(m(EnH{oV>{iv0^g-Mx>PtGOBmlXk|OKhDCXZkOtS7i_|9AVAdk&-Q1oAWE~M8)8G^*}tX~(g>0*Am%+ZliK^~g-LmE9mpeHo< z9Qw1qBUab!oo%+z4`y`?q!|oCO*O_o96cD-VfR?5cu7CZrdx#;ABk1_mP0H5`bO(o zz}x9%Fu%D-7tS5u9S!cK_s{?Rmj}1`Ai|kH_)WF_4dL<&_V5LJXKDHTDs&Qd(coAy zOV;hq5)g{#9`*CFUu}R|f^3+uwDP7_ApJ**{DVH;hG<#LA&16Zvpr^9X+fMnjFc}7 zeJ~GMcjG*MBka5^5?;|*<*pTo>Hm72v=-xU0tQPI;DNj}(r2nTyZY3SjT}tjAPWPc z9JPTDBRRUSzPtoGtO|{@tsMGEsei_!{Lh6|$%AtWZOerb4k;3+*G3Z*Lg^`8NnR)g z#5n&vfwq9a+u~c_pCg;F0;RGUX;Z5)L%r$Cal75Lf`tnWE4AA8BzBHp;Dhv3`%Qrz zUj=sw+pmQreZOd-k!Z@;$4E_mvGn0X1vdI2yrS6K8}i=-9Z9F_cTyyO@`zyAUP&77 z?%VrEH!MbFE=s?-kS8F!r+PVNx>vp-3;=wLERru%Ni zb?lOVILZ3WuAY9*`IMC33A`<%BK&Gqk`H}YZ*{5bLna}3@}pP6A**TI_p|JEO-9X| z5K^vb?2!U&CX802<(pAfaw{+aGnBO45>@9s3T$&nd0p}!RURO>v36G0=Hx6@3dLanu;L(*;i(SVxEna+Xd1x%D? zy+2|c9xwX+dePcw4x^Onl7WLRm!BAIo22T-c^f0t``b^O`bIS;iy=~nwSwjdJnNq) zPiPhD3?pzaTk%(HYeanL?9f!6IoBG*z%>lH27K1aAIs&qu_SoA6bNeLQ+}?*ybUut zJjWlSCE2xUmDY{h)XVFNp-*A$d4}n*cijX2a?X)x# zcTZGv1h?Ve(R;VT#nXS{x$G8@Xi%StT^SYsVZRNP&~lPve-3fFXG)cV()0b2QOk?1 zUJ~cL97VC68+fB)fG4U~saobCo#|r_o0niOEeO;sGkLJzo*5;k&9c51> zm5X;tXge4{7EK&A1T=>^Cka==E}C+_g%xzxPUWb0Xnwi?$a%R;4pp>Sl{daj*VH0I zmSX}2b{GZnMSoiK^b3Y(O9@>=95S~!-V7PzG$rs79)C`xf_Bj>hcF*yWei;X{hH3P zkuMhNsL7iyJGLh5+ZZe!@jBc8JU0xna%p%~h$mjdQFcc3qrij0wOuPPTDj82rZ!1> zj44DU_gH-1{p+E&5T3mS_j;~K3W?%WqhfU(N)BeX!na=%QZ#^=y1!oGGe&$+8@8sj z4AICy;8x1TgP*S4)9j-;a;z8l$F|7pRL3}VY!0yw#Jx%z3~JRp-{ZS?sNV8s0lM+x zKK?D`8mRV?Vmqr1KRxlzNsUv1DOpE3(AhLqklrq$9E~bw3~L5;tVZ!_IH}MJPVftn zU%Iex4sS$v@{hj5KoRjDpsZ)Rg7XwBh=QH<7%ARnb+HiF$w%mq41T&f+!%AsLjJKI zEjv2H5tS%xO!tUhg6cw3;Emy>%hC7XN*s*H+3S~C(0AE0(`OV>Jl4Ivpsi||(G0o{u-z67}nZ3P74NZq` zl+Mg#>sjo_fOYL zWG|p-)W2iB<*~<~oZJ&M_IH|^agVHfYDVkn+v6|62IPI>I>)WNT+;jqE_EKt_^Mxo z(u!@yUFL{hyG4mWk{GP`g-EY>6rw+n0I9d}E7>CHJl**ozSU(NbAxvh^VJ(%Xi95= zf$>PqlVbF71A83#&*z;|p_i)LPk;M&_-TG{yJl~ zm71s1mnEWC(!YnH#rtC7fQQ1oI&Np>eY0adl|sbzltlxf*ESlS9-51(1*^|f@ecVz z@WIs-6ta&cx*)I}_Hmnj6b zYtL%gEBQ;LZOKvhQc68ehv9%2bz_+p=5X2Ayd~Ja^Ei`b*CnDPiaqj&H4m9awSRQ+ zHm2PRt_>TSgXZUFC&vBoBJwyxVoZri5d={_-odG5zgE3qVVGMw3z>O+!8m)|Fz5?F~bl19hkkf^T&ogxmb9@yX4fvo`L&OMK?b=P#L$E{WmZ z@nP#94yH9R_R6}m_%!c6>&9ZA+QtiOS`PZ>T3d|NxL$@(`Ma%8TGz~jXhqg<-bs;C zeKV4BF+%N=R2Ewa6{!E7@n_P{`8bQ&7naxtlwV+}r& z>_Bq`?H!8%;~@q2HalMk|CP*ufVh0#OxReIu)xDCQ7ZA zdYJ?)odpYhc`8cRQpB)su(LW{RE*ZIV$cl~m0O9LA89g@%Q$4vX|FwFDui}hV+d*Im{b6s3xV#?;jRHafkO)RS{9CUsoo~04GQQL z`O$?cFGpg=IKSGUKQJN2FIjIqTn2RP_Qqb`^3z=+&(cHWN`>8*(X4f9>GB#P+_|qr z+S91Ql@pW&@S;LvZ=eK@u+(jfIGG;V`NxaThSYG5<`6N7;oXiA`Xhb7Q;#81>qDcA z2XwtC&q4bySVJi#28e9g3$+3Ggz#fmFXoP}4tz5$*7N274b6dTmz9Gv_czYqLRrU7 z$d%4~`(vbfFa41{Rx&|~Wyg}g`Z#*z`X^2=V!kn@IZjna==4eA+xuRqvn3hUw0vog z=|!4lJ$m6Q?6c&~MtBp;bZ>C1Dg0L-T@zV5Sn>d`Cj2~ipV}$HrOSN0H}j8!bRcuj zb$4wq)b#OGj1h?wyA0XAHcJtYU<(>pAHB9qGTRf3Ykqdc+?1knx{`aGQXLQSP7529 ze)?K$P#2P3ICX*aW_p#ko@SWh;rRL6ZD_2Z5NXx;vYz(*$akmRW*#x58s^W&AeH0* zS)}82Gn7_l_r@U!5y~)6x~EwxOCEM7GpAC1Kg;9Dz&B938^_)S9dZe^$%52PZm^d+ z(6{y~tQYlhZ@$JZCJK(HQdo3%;-wr5G~dgYaIeqoKQ?{CB1D0A2FJih?qU|tD_Ed; zK?Q^5c$ew|LLLIkI-=AYHSBPQE)YN>gdyqf8K0a~SrTU0Si07gVO)|LL58PD+`&<|y~)t!^|YFPU2-MpF1< z;h^|gXzysWLiQBTa${4nOsl>M%k?C@nvfRdVhb)BWTP6l-9->^uch)MRG)3~M8JlN zBo1iVTLK%?6K~N@67}8~(oS}!0EsBOHyzhUjv7e58 zKPS+q#wbFUH$yYa*i9x~FUpa6sb+q0Nr$>(=)g1avvRW*lK^Dzn?juVTlrO0nVh`d zoK=i=fha3_Vw_12#(vp#z_-zU5+BLPdWhy}$=d}qI=$vsLcAZwiUqs55v~$0} z=K8%j8GA$Flwxk6BVTirmxRslW?)URVz;@~jS9GJuERM@2832Zi&8~n`HNQxrHP=x z1fgNguqcUH}k!K)8b^gEcZAZpEFxlFQ6 zP13B=gDf`3bv{RNUig&LsEL96birCxGjFAZ-y_`E$a+h1J%ELV;j@5fT^imhnhoy(qQVhViB2=aMmdjQc zmF+m9g?&bKV!V2iK#3aQ#(xf%87i3D@S@(i^)ssBqe(_Gd^LEYVY=(ta%@4TPDUXqMuTOfB#^c4Jki0evJd#nuLE@|XcH#t^H z9VJO-s$#+%KQRd9F)WPrWi%I%y$YI8PPXo2bdhfjN3u|>_~Bn*p;_dH=C2e@h*2$8 zzKtY>s1>fglg9xXSVrJ`QpY~nA=IX;k|CvDWadcp@NBQbaf_pvH zcs4_JbHSx0em82StKKMzlL1gHPA+S;Ax5dc9F*V1;iD=X_|F=zq#eT~-=y}f4-dBZY~PI{1IoO@$AS1Etpa@K*{b|npL?@V*sdLLXsV1BT4 zPGJvUrJw(EsuOZ^`m%~m*ZCdT;~PQz3YHiP)~LwmpT4g(TE4wL;OC8wp1kI($j*3I zuh9ARRYhw)HlvTS{ugyPuLgcsP3e+&@u=Z20cW!(B`IL`s8Ws%suYw!mCO}WGkKfL zD3E-iB^~v$_aVo%oZBFOwL)rso52T{Dh3{kdS9k8>iTV8o}g-O#M>J7bGtaDv zGm70aSkAC_c+C)4R6KP}%60RuyqC-@>=qZZJRd3(&D^&s^eNr#K- zwT;?>b*z$2_k*4n;VlE)cU^xH5Jg_>bp7B$hm`ksl@Uc!l4H)nt2B{DUi%H z^HR1Qjmwd9+5$852jNwd9Lk!$=aPCm9dF5ZE8FXX0j&$^X5l<(_<+2~WtMz7f)FD+ zq~_9*_(S==-&|cn&|Y}dD+D*;>G=vNBclgtuLccSW9Le!G}NzXXg`J4NCek}_$Fut zs2Y^~-iYgtO%e*@W4uU-=N%n-NIRLkDlL>gc(%pt^Cr0_(%h9p;dJ3g`LXA_r;%Wo z7U>h>pBS0G^XgH;ClFMR^??C60xENoE9E7L?A2Fa%RT0o=J`!2`C!3qfN$ez7`@0u z5$IeUf<0xu7)N?9PAX8n&06?R{k@WloY+-vyHmLK>7A@K$*YHmJju@2k0|?m;=5Vu zQ8r7e6?Wsph5)Q>KBdwVZ?(X(aiOA5Z~P4>r^T z1ICvYi#FM8O3ySx$h;$@#CdmqRrru5yz%Aul=;)^EALmS4)bXIk> zmWU;g^xmo%*@$XeiTTiqcsrs3Jfz9bYyM$XT8_iXLkKcpO0VF1%(KJC&=l% z>KmInPr4k`HM|D45lcbSbh_Ob0oi=HNBjnd2dn~z_3m4fu@>SCZ`hT+qe}VGz`YV= zBLxN%HH&r{g@c#K`ayAqo^v+G%fg+6jar$AL*A3bLL!6n54z*YTmgHJ-SK$M7^Egv zNO?`|zK{xOt0_*^kZTVwH**;^4YOn^<%(iyfzeh&GjI4qG8m-;#bw7Ba-f+7B!e^J zg`NGfi1Rir`dJGlBPvU}+^dh+RSkr2m6oPeF_^C^#F&(J_?6IQf#I($$9JokcI?*- z6*j3mDB>cWT?(3ns3SwoE?NFPAt4I0rQ%yR{+p#0YEN2ME@_39a{I=EAW_v`Ob3-n zE@9H3Nv|lr`vI(rTi8)*oL^uNF8r(XD5`xT>geT3e1Lz&{MO2jdhJ2yW?ua#su3=+ z$I)kWw8lDVZ?9cEFF^nO#kbinUz$|!=WxTZ7w8=ByHKd(UiBkXwZ1W@rup6Q8)2WN zyK*mKLWwgv!REPtyK_=XHWsn{^rp_>+gle3(&=;|nrrv0ld_krbT3t=O+H+@M(UoJ$Dw)tfb#tYM33D=-;Kfc;$7XVFXVRx_oxfo)`}A)Sbqrc6hCK3KuMO- zh3ySt+tVR( zS96>9#p7!(h58Awx>}h(^g}ia`7;|HcilM90ZG=3i55-+YWBc5))rh>VK;XKZu~1# zCCO}{$o-Ap?a4}Ui|z+SfilC){)4i&Y65&GW4q$*qCPqHkB${H`I6ZK_3cK#b5nj@ zY}*hHR~Fwe{7jr)qvv&4yTwgp?A-4?ptJO?iAxTdNUYKj)?e#RY7ZMhF12ctK-ZR7K%Xpr3s=E^w(iFr(%4W6O$KRf zHLzyNblRG|S(2KSRoT%o2!qZo=B>&6*=FKbEGTIB$y-x-f0RE z+Nbsdiv+%Tq$vV4b9J+z%L=n(*|KGVdo$XdJfi8}m$u48PGa52I;O0%Z-iQyqFxzb zR=HWgynUB7^&?j~J9}YjoX)-?mV@H-`pRvIpn|1EM4O~b{M71s8-%NVFJIlFo-Q?m zrpNHFtY&B~wBS(tWSsvkY9!Kh%9X0@k&SAnQIH8Gu(0E8SaFgZ11WRm`dimbL=~hp{waFSF^*7rGDd)qGSt z`&=!Y3boVEG)_%0yUc~+xz-E{b;-4w)b2{hb_Csb);FNm|TesuO`gfrhygYag* ztdP5Sk&f-lM}I{Ye_D<&cj6{38EpFPHhuZbX~xZty+!FhmqqQ}*XLdPW|_%DCqWla z{mgp>^1}K$D2c-cn=TY5=AJicZCrBB{%cseo~Dj{IsFHcvbJ}p4M zqowJ~Wv)hc>NuBGyy2VVB)Q<2rsy*x1SoF@1$xw(T|@W(4OoOqex|)2VUv zu14pI-dA}ivo6L28_cwc925e|(gPjkbtWRoEDvp7b4w&9BO|swZA4*4etHWy)Fe*To2)+hBaN-#=POm8(9+`HHh}V=VUE-MF=< zEpkpEV&N%)L7>vXQ+QZ3>^LoVq}UKd?K8!%F6VT%j}>>&2f0o+ACLB@yWHY48o9CX zN}i8w#8b-=ESw9f~C4#lhg2od08-iCBOkbP*~Wc73;|L%;n44<{xJ;h%#XZq`*Ytg~NO2;u#%@0X z_id6q%+3UDTE>jF9p3@X$H;d+z36rXy_z63I)1IzDTsMYjoHC40z)bJeTbA|@`hL8 z$3f)6?PrFyoj+f822hd%`mS~SP_oqw6^_;p6O|3Y-^}f2;^X~?8!xQx$bUkP^qoRn z#}&sQ5!dwxgEqc+1(2wW=nD-PMjdaS{kHv(s9nqJwHvE6;A~0=Q+lcaD*M5vES4Gn zh_t<8?09Xy7v!6knz~$yc~Hsq2VWinn=?~$p9&TajCX-{_nbkxObQnUl34kmm}Viq zsGLNHIJl;lkdQ-v+^P@H(}=-L#C_$^i(7}+LSkeii&a`43YQECk3Z-4x7?iIw+RO6 z4^lZkmd(Y-$q>F?g+4YO1GF_Jd`Jid?n?`#Hg(|+RU8g)1~zrxJ?+B76HiRU4` zfg?{aUVfq0UWZzlrUAuWIt-)7T9DmjN|nu{EH`sf?KCBgd7JBL-OvV$2tGDWm*rVz zi#PVY0dJCHdWOGkT)&|js68FBD#CO~$GoqU&h|tQ%?w&&WXt2*(dxu;oaZ?6zx*N? z?%=^UX25M%V*qkyONLj=DidfrR7C-zvj4X0FMlx}y?^GO-C~}HN*FzowIbd?T%h*1 z2)WEHL`$tFD0tpg&h#Z}5OCQ?%{#yu^j^8dXPrDRN(6Fu?H5h$V`Bo2t~^{a^m%2V zO0m26bDOzyo?YNDR{VmEI+89`3D0S>h*nEbRu;dsulLGnKGAD=p_xc!0$8HtCNy`A z4hDX1pd9s0fIYFZ41w}g5Vsmbq$;D<%3r;&m!F7SW{ssFKi3sO6$)Ib&nw7V-bMUZH8UMW-%16-m&L&aIx4`NE`lFu! zo-4KT@Gv_XeJ%xanTDC$KI+r{=qRQ(&^1j_N#bA^g58v7J7sK5h}`d%aXY@y={jiR zXeQm{+ZL%jy}kZ3@8Ro-V#@8*;PouZ;~J^T!51u{(})_Uo%k>!`p}V@Kd>X1%B6+4 zce5qoIbD7LsqGz=DMBfm16y_VGO=$}rr6-@0sAU!g+&nKi3W-x;!QP(26{r-8xQ=KZ*8RyX9czpP zzmr!&cpFM~Yd^+l{GA$+s@fNNthqN{1+*mR2dDRRxEijOEt_jPF&#Hdga6F}*f`Lg zf2HeoF1RNvCCqNca6Gn2=4X*q*;G!~5iqmr#wAY)ms;vybfr4&(g!5xWQi{KG~_J+I2B{ zLS9)CE0xu}F|r!=G4SbA7xwUzp*TGENe|5_lvy~SH*S&9WRG-*dvu$jJxy4c@-${> zj!1JB@P6LGWn*R^lHvG7SZb5UhSI}(hT91j3iqq3;vx^@zH4hfnQ;KEGT8)*cMS2? z0Lk$(np5W}W(GaN4c?o;e3Rj~mkM6q3FdJMr^Owmiw-?H$Kc$Slw2HYq{jS#i$B54 zT%FDrUaIrv4Ubaav<~{3s+g1atouEKv3r~8Pf|ew{x_0{ni%Qv4r|Y8^?)nIxBtXwabm9VgrWT6vr+Hy5~TFeoZ@-gUqldiU{W@Sw7#(YS9ajlHX(&N$-5hGJC`_Z z)|Z+m`gAD)v(5hKgD{O4&d$pjmjQIG2*RvIuJoflAhvFtA00zYvMK;4@7DH1;y=1% z!ya;|_p1!L{N!-14X7i9)J@RL{$GLsVqXo3 ziF?H{*EM{6zi}7!w5yhAnzKP2rM>}}+Q|6&o2@S`(>aq;JYG&&w_mgpM9Z!OalY!1#wG(~^9 zSA+E1q?+Ss$;NUE#?Z_-G5NdBm#AK_i@hn(Yt-=YP%l@&vHlzN0h9rID&a1;mf%9+ zG#!a4ZkimTS8(s1qp5aGRPFxx(>4jEM`~l6Ea}E^N-JBh3zC|za!DJ>XMU-X*0iIt$654DdagQywZ6KqRS9T9EQ~$ zG}vz605p%LK!5+nzP*j_vgPchLMdn08oQ>j#i+KWD=l~195TpOzO@0#mJ5{SDC7nL zx)!q7h@ox*hKkrI%ojrmHsR> zr8M#kRWE5nq5_0(gSy6uy~!4W*>i!j9YC4S|5YnPNa5dX6T8co58YBkJ?WrEivi(u zuf;JBuieNo$Espgm>Q%o3LEP>dz4lcJF7kcKAA+^#7nNCsa}6 zML$8$cr^bFX=%7zMAc}DOyh5zPP`KEePNDCS&`m2(TC6A-~V{wFNY;O_0YoSzu74N zg**AbzlvX^M6&NBH~xG4e~zlbUgBZ-0gi*+_&56G|NC=#YSZ&c^Z|_T$bXS6|E90} zk0C@qTHLAN*YMFz=Y5l5A*e&nm0or=}xb& z8;$(hV~p4vXyiJ|zuHOQEcm!K{Tnt@yzpjs<*>t=8f^~ z3(+wZ3j%xA46x%A9(DKv>)woKL{Qa7Gc5lZ=s(~1>C7dIid$3Vhdy2-uc_y~Mh*cO z?g!z^r)w&mYI`&td|7|9UELijmmY1Du49t^2%FiyD=*swl05DI$j&*m1?3xx)gKEZj(!S9#GR!X z2&uJ>CwqSWJTmpG|LlL<2LE-#o^jFQ+#a5E{r>AoC%pKWul)B1mA{%tl~y#1=UC2L z)Tp}q0ntUfRY`KXc{Bj$`A|vcZy2l#V8Ne$=1~0z|7EQAkSZuumsRb(4cTffp`>hP zO3C~okZEn#C>3Y5x`(|G5Kj7bqA2dG!ZR z{2RP{v-!H4>1s50+oI?e^q%fk(^`YD_si6k{qDdGu^`(0JKu7=IY*;KjUpZCe zrq8jNu=h8n7xkCQqA*fkk&o2*z_XJWRK2Iuqnojzrh93p_?FfmBFe?3AjG*pPp6p@%*g?a2H&19Q^)9kdJ}^EDU_O+okQ5u&;SO-N!a{(D}df zEBXuoM63q}Sm;K6ePmG2!u(lr;hi(3xrbkZg7DbHpUf3*$)UW{JY`<$Sy~NQ!W6xg9cSXj`XL4sQLH zI`H#T?u>SOf8#WGhc7QL&Ro+?R7sZa@~%6aq}T9|(vi}ZJ4=RDrWWjg4A{cmSJ2y2 z!0ZX%`WmRDdfZ)*v^JSqAO4iBf8Q_;c}fU=*%<=)!ufowu2S+k2^eemj&7M2I6^aYPtCtpb@BgF0$Ob@c+8Bvga%3 zimq2GDdbQC1r^S6BpSP515CKWe1*%tsz}^7Hd=EC0!zBRaS8{}{muVE)%C{zvMs4wTmSyT0FtG}E@QD(`8CtH6)3 znijUu3ahzKbEoC#L*UUr-+|DdTpfy2jOh4f7E{*zRrAoXqyhk#w0620tSq`cf>Ya} zf|lbY8Y?ot?3Libp(3^B{?UeyYUMdkb zN|Rq(^0Q$lV^1jE*OF%ffW;&5&&TP}aaH%r_9ew*#J?1o?X%X|?=ss8sbClPaJw?y zf_63E=RDOKK|i6Yn!9PK(2*i+Rsn2xu&NmQ&c#S&crznK$qC|9Zzl4*$3A~4f%unv zfkqHz*xXZsMl`u0=*=cwM5qdo^cp+QrdyuQkSu_lZne2gq-0rz zZ1|?(cIFO0fUy{%x9R)*!m$8t`xiXcARFsQ1CFVRk6Uk{$^VNbtC9?)JH$111h|LV zjQ@%?BBt0Fc(MS?&e;*XJsG4P&2;Ed-sSXPl)L}=@IT~@l2$6uQDymHrLAPkm-&}S zvz;b>bpGZlS?*d@o&EB_Vt=FX_ikjPi#VdT`m;=_6|7$$3_YH4a--J zPbK|#GHN;hfbx2y6(oTQdul~Y4zZ^6-6B(&!xPU&==uG=mO+h%N{e! z9k6l;)C5YBYrE_lx|DIGOlE(`-tXML$k5Tu3PsV(VGU2DCymCox{_U`| zbXLWz+M2JXiO0(+KKJ(iHQ}*>^Wj%(B@vz#ng{$MU}>A(er7u5GV&>C2YminUiZt_ zWPCWw-AK^tyOy2!_xp8g=}_dC^dan(zBQ;Tr0YU;#19E`BcP{@C|2nI&tiM=IH@ZW zS;RK`?ndLG+wg434@Ofy>5&I;crRz)UuId=;rJ8R1VyQw&x)nlox`ZBwksuab_Y*Q z&FMJzaLSe2k;#KMlMNtShkku#G*JZ(1r=}MWzjG`?cE&n&hvwsEXN%JxMAtfmgYUL zvCnfp=b=VX@HuYk+N_S+ZuXf0m0Rw+gtQS*<8n*F7Gr7ulbCg0md2Hz6}1EOWw>ha z!_=^a^$4365)~}D-|W?mk!~4rLZPlRvp$BD6QCFhRGM*l;^mh+^SPBB+EUqV_1)!I z`#-;LyzoW5Z_KAFdt$ntkId{_8MC}*lV9?&HKrd8ZvvLr^#e-{mh3|>*b2YPUocIe zUH*_-*cKtD{t_M;phzEbqa?fjV-8s_dHCuxt8nc$=(|z%^_~~Jf{1m-Pcw~5rJjGD z8!F#XtU&hvVg57&7=6|;4n4hCDnTH@xV5^&fOXgXvBL3_F+;nfiG!Igu*wJ)7MN*w zc>BX&l2Ypq^MKkA7^OYpttPAIF414&6T&xo+;F@{g0i&@$V}&-|0CaIW&!p z#w>l6Gr{2zqw5;sYY%mc%LC|Mn3$X7@5n20^dW0rP+)1Jr(kQfUt>?2=Tv%Jgy)u< z?~V@72p426J#yk~$IUeKbV|MV_HISVB1*JrJn&85HO$Qc5<|<>l%{z>xc=AmsBrW} zeqTl`xFCsSple-^V@nX;mu4`xRe5nV()2# z1wK-%%{({ca6(S?PhNwpVoanq1tt4yd2+3#E)1hF@8dg3l}g#O8a$p_PY6GN{qJWW z2ldQuv69|rD{Cz-me|S)1-=XB$s9jDe+MM4Jz+0JD48!+UM|SVkiF@fTRS5ZzVt{i z=V_zK+NXzF4aBqXj2dEy=^o71JZ8!8HEGMO$eFpY5ou zbGT`R{9bYsL>r*&1x2d8mA$u{?WjKecA`SxY_H5`N;718ueGCc@SSd8ajlgzbVfH44SOs0;Iqs&hF^`|4&MPjSe&f#PfRaBW)>-9- zqi)_%LT@o2Es5|MFTl1Fb4S5{h_U;4X|3&?$O_b<=fX~a02r^c5plX#=ymYMX(i-8 zx5r~5HPYq}!%BYD-?mEoIpeJ)5~19){=~+T{LHkZX`3nB2%5vu8TIMhbosR(;g>RI z1UYy4zzeQF8cpY%VFOpu*yu6;+Y_#;GONjt_x!hHb5;HYcrp^~=v5&t&!Couxc*={s-rIm zf0i#g6qU_=_SN4R`D+t1t4AZfi{NibTub@=Z`+om4#%SDdO1TSR+g zjixd$jf4i1a)vuzP|8bFat=oxea_zslF`aY6R2Q4Zoa(^gRKCe)8Dz~`E(ZW?r)}_ ztyUF!`9EO4$Eryyyu-iGY?8~;K6<9MVizg+(W=p}5Qt&lX5_P0l=Kwri=p2KI0*a1 z{Yeuq^V>=%e1eTzM|pMFn+ys(?q3uiG6)6RXN&;hLGlMR@1pE{U#Na0#Z^~O3Xi^i z{N@}>&jP3H6;SaXJj}gwQ`mp()(BXskvMN=AGoOeykY5M7KKh-`Oc5^TBU8iZ_GzN zVgzSeX7ZB59@?us-C^z)*G)Y0Wv|uTlDJ6sL**aC`8n(D>f8-5*Z)2DuhO2n*e)7J zcKMC-?)~Q6MLq=f8|Uk5b&VC)f24NXl{%mBYJ}YL zktR)q*_}gEXTEZ8`)%9QpNkGNS&9AKtq}8Dj)o?tke=R)e#dB~{rBw&MWz#xst?$Q zK)(QAMQr={v+9%k+Nl59vw)#I&9qAzfp_%R)B+6ARd=U1Rui(;|Isgy3R-Pqd|}C&~CSt<5MqzQ>HNF}$vr3r3{S&L*!JNAeuirTC8Y!yZAB8u2Th1h#U>>WGyefj?G z=YD?A{d@j%f|8utz~ z)y~9ocWImuN>d!ECu}A5z^CI2FXo6%kZ8h62}XAY#mOI+v%HgRacD}xHp4ua#b6dK zrq(bvxhk(*#uHpYpVlePYK9Qce%yS6V=t;<{2a_M+5Ed?ZqH)isUaUtl8xI@*ls6PO z_kOwDxc#$VpfSA4cyHWBj{lC`G>)G?7ZmK$Pxc;fRe|s>x?pnnKi}E^{l`mlEIP&~|T>0f^ zmC%mv8p03%m7YmQPKXI;`ap)=IRvzKeU!sS`u@kE>+&N)LL2q6GJa)nxcy%@{r^(J ze1aM;#;lHSx&HsY`6-S>#e`p5OB+p7@jrI7%TGrrn-uxR&R5f%y$wgd>((;mKduQc z>X~vtk8QX=K4Z_B`MT~am=ed)N-?*88TOI1_mryCDH(|ELOg-|N&51ub>mS;=OY}IlHb%2zel(1}boy^37 z5Ey)NOV&INx}Z>y*VXl@9x5btbqgepcJzIm3rerZl;7H*Q&>v8>p)?3aE`q=)#|>!)9SXyd5%_P zU3RvXLCJTTcAfHP*Qw^Ru6qJ2$dk>l^6_;)g2TlDkd8UUUQ9lrCCThuvWC=-7vni` z0`2r%yprzbPus>X0lH&(fW|bY%)S#nc6r9t*w}K7cBHSoI4-#`h%?H3#+gtE=$J!6 z=aV01TKtVqt5z>Hv%g6?{L=ucG-V1nx&9)}d$0|ll|Kql(;N4A?qzex)$YWX)Wynm zt^UzD!!0RX3XuvKXTV+jSKzPA4>gzSX>GTieTAa?`EiG;{F|^$K%0GE5eA)qfY!JP zh@i%n!0~j1@4aIw5(#iI#cPkc9Z-tHnq$KAX)-r)#DfCd+Xb`3C??7vpzz`!=_7Tr+3A_;kXTBRJf>GS70Z_DciUq#!4)e8q3U;2J6N7z; z!E?vGhkV9IFO6Q5bq6)uIe1TuDVz-`u#Idz^9-QzpeAFT{*9cbpxZv|c0f2E3}l|W z9DK`@%^4;)Os`0vJ59`VZ>jl!=&?!P9^H@2JpH*Ch(JWi!pz0?03`=r=`G<4!s>w_ zW=-uc8({S52|ge+KuQ%VP~RVLH&HaU|4so`WD)Eq7&Vw9sLS}R=&5Ho76ZB1H@?ud0V9tq$*^{lZ4JP^gJVsF z480dqVpnA~ch8v~G5&Ebdurau2J0b5tHB?!-IRn@&Gfo8vxb{ z4H2EV)cYQvzq6Z-s9Q39_~N@=jPr(#v3CHn>f%xiH>d-&B*^cuTzXm-&2ci0N6#)- z?g(GzrI2a`XuD@Nk*m7qnM~?}HJ4hLso&jOvRyLZUT8XexFVy{91`1B(zbGm?DT>J zl2)3jzXlJ@>Jz&p|x(rpj6Cqn){trYp#(){4;F{I{SnJ0o`qDY?3 zecop$F7tP4edzv__35Kw0Z{-euF#!vShu7*!ZM=q4H@wbffpt zF{k0xQ$T62de2PX*s3LarMo^46@bH{MN}g!2=`IY{nmBX9OHPyQD)bmIlAztN!N6ZG5V9 z^q!cQeiv5KZSg$wL%Y_@l!se9^fvz|z636lI8J#@hGY5KIkqjmF#r4sUL&bm%m+c8 zBT6!ePGXavUGmMUa%bjqdoJ@;qJ3w#UajejeFqdomTG-DCt1^M;7jK%-8LKY*YFL@ zVT$$ck=@03S+zabgaXL%RJlcwsC<>x{JabUi9`Qg1ft5LVnPUOsBMrm8c*{+V?oqC z7HQMc9uqkERwt>eOUSD)Y}IVI`7QNiC%3|*^JVYl-tW?ky|6y4JMSR$C!ceH@5G4ShKJ5s^+~ddf9<1kTmpw}n&FEi2it;_fPEdxz3S@fWBU(4 z(8Oxs2P!fw9vJqF82fJ+Kb*ja5rU9MWh2=CauKE0Ayg z?J3f)g=J+!=5_2h*U2s)0w2HOyFFrKiczxYARtlk*~0npa} zRmLt^C8RNju>t9$%go7L@&l?$rA+a-%QY}!C=ZSB0fvN-wm;=#B-h(Yjr8;ywVM{K zvjUUP?-ZkoZIzpxG!Xqakl&39fOK4?+QP?FwpD*s*qAF=y>ddzNr_e>y zePX+nnWAxB-vt0Zx2O#4nU_S@n6<)^@Zb#V>52he$?~=|?gq)kTT2eZw%;Ua*i*M} z3)s1dI2SIWi0PrM`?lt5jBdQ~2Y?f-;TA{<$r>ovjQ=>345z zyBg0uDV~csZN~0>=3x0l5HbH^-s4(LG6k!4=%kr_;ale`g#LhPl#)kt!G@2+gj!rh zLcLu;amOq|Wy4Wr%W~69aT=W(^PU^Cu1+M>v(EStn@nbig1lAjVp2^g^lZf%g(f?g zEjS%9!^oRk_R!iKb1D2e6$zRHOYY^7)h&;sDNdH}`G&kg=Mi!W>+WR)1a5auceh&! z!$(`#-E5hNV_y#7iM5kC_Dnq0%Ug;f_P5IYjZTRTs=Q+YX77ISCs&{6GTn3Dj?A}TJ0(EZf{X&zTfIYPS8ar0NZ z#jAgRmPHgN2M}Xs97X?H+WNGJY-?39Jz%l4lU-Z$SfpH<6=dtM+XZdnA*EvSb+gs! zK{JvYYd7GhKtTlJO`6y6NXNWjDk1pa^HjKs@BWVxonN)xSC8R9PeRe;4nq;}EMTt- zF~09SG&#j-tr7N`<)qkl!s3tVEcJ^yWY!>{DCTd^KE*+mOo~V!8*znlQmS#6$h}gs z!}ra!a%-<|BDsqy!jmVR7y$-*w6kHLbLXGYKd(4xxr9h3F7f>aSsr$_ zSJ!0jr99i8=TWGcUR-6W@$Zipm3|@H?$VYBt+xLe9ervfeov^@-`-Tj-nJ;OZ zKHq$+{f@XNYJh93X&&$nxYl<+6NLlY4g5E`VBwF}^W=iN4B@x=xzj9h>e5NAM|w~_7rA9c znu567Y~gv@rL`0+r+uUMiaSbUzF|!hlAHru-M{UN!%p6phxCh$9j6F)iUbkg!jn5p z6?3;p)_72p{V$D2`;0NQu~)=Iw0t#;cP{%HAjPFkz>tpji+XYL3s^tVEob{`1J0)3 zZGyhoW+%wV=9C_5MrL{WtZOqFMpZo);wuV!dD}{sG$Hp_%=OXy62p17?50z=#u%~iPF<>lA&F?p$LtMbb>ROwg%2_Y zUEld$SQF}4*C-BoIWSLE_Pqh+@heKwpgP3w)b|?nw!$I^lBgxU^3#jwwPTC&QIkO4 z&V%z%arY`D-!lxiU_u|60;bTule>%c^8_A~d+mJ<{s_3omA2<1wOjF(P;PKIPf3`m z@nNrV@gGUKv%LWugzyFKJARz?m6eE1c9@EE=2xrK8^S@Pn;pJRYANR~j;=SC z=I@L970PTn-L!OZVX1FU**vwcXi3Xn=7}q5lQ=P|d}7s?Xk?bAb?~FoS^Y$M7jeSu zL+Bk6IBxdOKvI%9yMb<(_K!4QpIP`6Asfcw9)x4T%o`&71KY@1=iQjB38H|@_-{9o z9jQ$18eos_v8)^$85vWeFNwkmwm(SvRZqE#GtL!2g{gv)e_4YE1%|7hbZCoE1Q}u zZj#aYt946KkcSzEY-#iUjNr=eSAMVInpX$Xq!By7pQCfWmxBv{#ics;1IA) zp^-h~_}r8K9hXdN*|2~f-jpEXYmBg*Us!fA98J3R5}LO>yd)u77V zjM+iJdG!t!y+mNII<>R!yb(*KeC`}Mvh(uvM!>W~pVgF=e5~>jT_WMx=DgO?%xP2N zG46AT3hw06G$oxWh4tSBDn!QCpXxvB^pzwdbH34#I^x(3Up)C{f#c*bERC_!Y@B=I zl^kWfUjP}ZX`TGxYZkYD+0#;q>K}P9yivMnRc01R2U#3&>j^*okXbRCT;tK_x%j$L zP_QKoq54vAaRRp_riZJDxVwF?J&k9RbAhTup(#xWpFkr$1Gi|1>i6Z$8_B<(sJ*;e zsCJM09fXe4lrHu`lWydC#!#g1$wN$rDDl-fHLztWa7(0twX3KliDKpz1kpl{TFJom!`Z`C2J!eF8v!-HcS^nol^k$lpqOngnR_L_z25MI9OyhkQIArMAy6N^dU@fQ|KmVmFSMXkU zZ6OJ6q<*s5<>WTi{J7+6Hn9r!dF*q-R@AP&6G4a1Dxn}$0$hLo~Z^3a6GRH!30rH zOue&UoQ4IGT{J?o^E~_m3k$S-R(00QUiaZ&W|fdUYCPR#I~9*4ADOz11YwqQKg1}a zn!k0s;WCdfR_|EVPr2Rc9PXl;)aVXDi=osrzR-PZ{V@Ict-T7kh2qCuRe2XKjeyF+ zUi{63+WHnM<+2>>;}s(LQyUYeNZcE21EQy^?KkAUP4ISpl=+LDhlOh4Wha%&^07kG9wWOeziE~GlO+H@0t@md4YRz>>w z^vvA^Qo^tR{x^8HmQ6;0y{m5V;W0i^MuN~}7QiOSwP{3EC0YLYPC#U5TnSKk6iH=j z?^l#G#Q#;67=OXD%m}BETC55elkG!7sg%p;fA9hY6a#hyIhV@BBW3Q$c>UUeXW1*NtVd1=Hs4@0<^lLAsYf)0< z2e^d+mIr#%b!_!g*s7sMXLHZj5K8#0BdL5)F75E)LQPVG^x1x37L?jPqBCE zHPe!C--~CrA0CcmsW{J(s9aYx&xVx zzxMLW&o01&o5|1wtTv9@tqBF`MuM&$k&}4vKIT7I`FBvYulEQ2&PU)=@~x={Hm2k< z-GV{C9U78i*IH*gz>)dChPydy}sq?`INkcI-j$f{ggTwH1 z9&HvGDz8lKY5`NZ$Ih8tS$@bYG<}=ho&Ev0B80RxZdbgG!jS$+yZQAqImZ{Q&6pNJ z2M3n(-U&Pd$SHKYZTl@k*b@0PxV7W2ET_Zku!uEQMq@MbT%u)DGyAE|GaNg|M$mL$ z;e;cnU#{y?Gin@e`F!^x8>FMj}iGYtW-Vj@#2fUyvOlCo%AGB=0IiIf$$WHuU23 zKwvV+FwFH>3adow3lD5OylA1Dn{xw(jUur%ZAx^Jj<0{Q3`|n-{cY01Ol0L;M_59M z|H#|j+cZs!%SN78m`yJ3BCOXfw44|-N~zjyvuowSp!v=x9?MoDkBo&<3{3`4?5yMN zIYB(D94l6$BviZ1Ua)p(6(cPBzD$7YScB!L|5>f?vmauN?+W$`ILD~)OgL6{d%kZ` znX@4#xzCzIW&1F<+slu51($!=`xn9HpTPJ=*!f-AHw$!!_}H6LH9%MoP)+;Wfj?eD z0o@#v?kaEUYiz}Wm~uFLY!ANbh%{6GZHd#egL+}zV80jdC~V$nqJrL07Iv9!AF&t| z?3+?p7gGqMyHHnydo?6$bd8G~Q<1n`^D{Eedh^C9{wqm_(I{AvLRhV9-YnJyQzRd% zSq)E0Q$G%U(6s3p|C#0mx8M%fliEr($H-UsI{_vK{Dc*LJ&lBvtawNM;aI|&pV7HF zrs_pr@Rw&~$ITAaDIsJaLa=8jPl*Oedk`Jll-XQ;TVN;JU!xq7mMn~OP!fKfmU!o0 zn#N5J^E=Ulu)KxOA(;6zHr+63Nb+RCCnatE*&BT(oQw@}yV%RYDAsFK4lr_nGMJ%oW z9-S0iPb$c2=T&*6Xv}hhmp|8K0-IT;A<}+B@=+-CYB>(J-=UCc=%VYGx87_sS95Li zpMU#I4IU!_;?UP#A4D$cN-a{W2#3$hK-4Se$XO39o@T zl#72!&BOz=uJ+Er{9R)Igfg2Vjf&6CXF&J|ZJyZq6$pxAcY+Hi?d(XdQ&BIPN@+({ z%`O$Bz#VZ8o^RCu#UWd!>pz0&7TnLH6ZwDa_}I+t2B^emPL*l$eJF=a(ZR=v_82V7E6XvONODeZVnA*0DE3&qm5hWjd+o+B)RT$hb zkSws+|77y@);#qilw;1&h@F9Nj&H`0VsTyZo84bIqj~Sk?|{FlBA0+>PIJ@@PRoXWQ`@OWcw~IzjqHqV4ij^oy z0{O#?A+vZS89*XJ>pcph4dFK5xsxHeOt|@iuzGD3sp9(@AGa}wtRZC$0ihJ z3>jL#61VnFrLHQ!Tz_pJtZ(?)#&Cs`+fiw5H-T=yrQz(p*yQa2`<9^w2f{xkRoKFT zQc{R(QgT+CEM}6z@&{T@X54oBbusi_TRk6bs5RwE-}!~o;Exw^OO8gxA6ewbfCI8P%S)g^qY(=# z1gA|-FY`Up+3x=b-5b|SuJB^#Ibdi&DGR=b!nTK~!boc-M~^t)HUj?LFK5qcjG z8RbL7zNSfmLp}JT0=w`9J(mvuCpC9{0|MmFSQB=8x~;&aRF-%^tEsnJ*JfF;ejIsZ zXI5SGFK+RQXaB`8!IiGMSjfO)4?bGFVLz}RPePf?>uV@^yc!R+-|e-_v5iqUjpGql zJbfYI11}jA5xKuL`$L0eH{Ya{!dY!>E!CyO0QIn>J4&2&!FNJh2Jc1 z`PJJtcY2;2={K7vHve%r<%X$6I`&3wa1YUcaNH3tdzn`Jv1#0YsOVwnJxV^x;c2Tg zkEKO#$xDsIod6H0rXb`YU3#xKNcOYbQet5m$zFy=iOoMSk$ ztQbsh4d>lth_}C;ZC=aVoZIcBrXcvc610ZGB0Oh~e`Dj;JK#5b!+z0MgzW{#70QA? zoF;lg)aZwI0wlo=3sqo6taQ^v6O=E*!>*4d9d#6{$x~p-40W;u{+4L&Uo14 zPKrqO`1nE4%g!`&@*&MHs@xPQ3Wxpp)MkOdUNb!l8%H}8Dj9qq!A-XJIv}|1MDGeC#xdzU=XV=_;(A7F(79yuTwVZ}LC_7bwD{DAmQY z785K}e;btK$&lmjZT)5Gau06%;yF;4SRyTz7=>Y6edhV<6lt*b_)nc6z@vU>ooXyvML*~GkV zQbI3d38rC{iWrvzs;IHqbkrSN{7S_~IW@Tnylm{L_Hg8yTc44ROhikx1nuX>?L;#7 zt90ZqaIXsN$!}k=Be00pDszMV?la%iR(xctM@tM=^Y5*>R%q8wI~toZ;dlHquVhNl z>(RmFX78$rx{T&tFG=N(7OTCXz1L5c8svv*s<&0mOgW6?GFb%FG@C`4fhpy>a6sZ# zokjDKY9=p9Y$>M#)0&m2NZ;e(Cv;>NgqinMAV>+^Oa~ppkK$lZ>HX$1RID<3gI^JYMsZlXXc{V^G*sj zcxKYYcsqirTrFer78&?)@jM-T;?{&)csfEODtu%AM-U^03_`f!52eu3?BB`ui{kk` zs!SqpQU{IXD)cm-uiF8XL!4;060%7?Ec^7@xXv=5Y(ehKhr6NXarI3~re3Y^)&#d( zQfN!Y8c-v05Ijk{R0!U-V~=&f$Rr1Md?%CbY}2T@OVjpyYdDX_r*(%OOk8bXJfY5{ z(oM5P|59>WiPB5-h6P9ZwA8fTHQc@#xo%S8d}hvtXl3!LM)hIw{tdH#Aa1jCkt{Sq zgKzJiZToYaaBHR|B1`YN?ni5NzMO1ZvWxgArg3Xba#v~+7a(CRB_2kqH-iZwa1Nve z$m|%Vw$wpJ)M0sv)kEH*qf27}k#^Yu2Kt(&zTm^C1Py-auR=9JwiqJuP%hD_hR2qQ&OPaYs$EI0|Gh+dT0(XwSZO zcZz#PwSvwfQGKV}$+1ItsLn+MMzfghaa^^@Z5M88MZVcT8YXJm6y!4U(|mU$ zY}{Y$RCVZLYX9Ml{|+cRzJi}SXLVv26lSt%dnuk&ha;z2zwmus`S3tr;b} z`NJ56;yCQ%a~HbwGMgyuc4k!7zlIJ%fP(D=z9c3WmR~!q>JFc_Bj1%kFo_p zkoPTdChS4kwRDb=)EPrAs1)3irz09Bn(n~vgpR!Mdzv6?YyX-jRP}7$HI=1ZzdAX4 zff_vfyDrY_nr1sBs(*ylayln<@&Qk0?P&@7tFwEF$eamFku>5R>LYSVN;l8n3)jzv zlAS72#*JPs-aFEIA5?q3css30S!#OseOv2arl{Q>-<#{B*BCq{_##PgojjS)xKe*9 z#W~hmS$}0w4Pm{k$Mh3y+;OPP;MYvR3D$qS;kHEI#CBfPy>H(**#ompK#g~P8r_%X zT<*}k_9TQimtmaS@nW>o>YCxTILS9jm0q^{2GM+YE(P+JZw^W;mi`5|nwLv-<_a`F z@yatksf!otXSC;5uI?y}Gsex;{vZg4G-2@9icu%m#C&kL1e#InSgtxQL%Jw49j76! zow!Lu+!y?wV zjp!!Di?6-@z6_1%;eJ|6K;{zn#DqZ+L~H*F*f$T|ud9V0tCO$_q4%)!L0Tz#Z#;OZ zO}VFAvk)<_@=O+;1H&ic4Keg|D6s)EOFAXYS7onukon2W#&ZwNiIVl3t|eRGTGmlq!3`i(;)1|wV@-zp$?7X!RJ7wYi@WIb2@JZYeFgcJn^l0 z6xy;%BVA%YxfyGj{Ns)<(9GDU5GG0~OUeShGXU?J0a1GRBI$qjR1=d8vQ}*dVtv5R zO@bipFQA$$$RVqyA49crs;_~u`OUU`|MV@pFTIem#Siy>ZInXc^P#-yQt9<2WluG2*RP)m~zS(CPGCY{M*_*7bNuZ;>b)N;j$ThwZ z;wKi-6>CIrqI{3;sMTh|^#wi3Ye!8F(u^bQ5U#4|w-3Vv*$qkt2Bo^-&ov#iyxDDS z`#Z0Ln0Gj3%zeX>ci#x%w1D-k{eF8W2+@?x^oJn#22>4NZ5Pd4xbXXPY`dQ#H~Y>7 z>JnMQVLbngk$c&uxNMfYx(ruk@$(<;?-X{;=rD^(s+%{X4C4gSSF#iWOLr@ z2`Y1*4e|Xnd*ecHusjv+CW(fHeIvvg^#sS0hpW?3amgHeX!l;FO;buEX_MgD& zn$l!@hLS^p)J!_lUf^|(My2g?>xZut9pGVjdFLgS-{0`Upn0Ls_)TD7{C1Sa|Lix#d6-P@%vp!*KC>)2O*L5>p)6UG)MV1 z9yy6YOzufh&q_CZSu?u|alB$Y%EunTmlryxdv|K=nUJg#>QFwIhl5$@l$Ha&{J1!H zw2@hggskn!MQ%rwAiP!3t7Y8#Y(G%tuB}*bOS7I+h~Dss4Rn@IstHd4{xn4=86fLILhH#o)u*5-RvP6?^z`R>cSf~^lUo2eL&o)0k6!0t9+7~!O3u;G!f zgoU}*T<7soPKDGQr+Y>)G?UB2D1p1-Vhuwy38&_8NYr-JxxtPmJ~qPR6TdUm`h#?z zU&G!fPqhS;wl_?r(D1D3z;uG;(`~>$i_$CJsbUjanS# zD^mwK!SdJi#L8Y#um5yTePkWXbuKv)fbbCm8sQ-d2Ycp6;r2RuWY^0L)lbUC52er* z!P^tghODf4Q6Z&=&Zi(5S@d*=E!t?mvaC}~Gfan1l>=oo_`^`x{-koJKC-8$&nXR_ zM)33)bK;Tw!}?0Wt7HOC<^=nUh?zKPW-9%`LoL%|1q6r=&!pA|65&zAF=yegn?(^W zA*U#ZFy>@~Lg)FaojZv-XK5U)dtz0&Tl73@O*=eQ%q0tMkxR+4DqloK@!vfgzB+Hz z95WX#X&!q}bNd*h!%K)2g&V7X%vySoDm`x>c1c-$a#=r(cVq|1=%Ows8YJW9o59~{dgZ`i;@mcd;8 z*xEWW(eV9O?g6FLNZl||Iq9|lmb|DLIUH!myBODedL;JW zlA(eRg^RN`MH`h*|C5Tu2ltgm+m%VyhH0HvT|Ze z;aHy@&0j8X%XzKZH))~z$=Q6%dvHV9F%Y>fKaX690PN!Nz6#Dlu59L1{~Ju>$>De5 z{qoaE?c>SJ&Q3DN>47;ma%D}m zRbw1+uTJ2xIu5Zs=G*inSE-GLZJ@1Y!~Jgu7kZ0I`gw-0rVlSvVTX7&A>wq3 ztRbI>f>!&i;8b&0aMcyKJ3b7o8@_Wc;YI?MN4JT*m~bt@+Le3<;WfJeX80J$@a&Nr zVow&8B~;_`Ztqd7N$!3UGu`ub%Tx|`t5oa>hr>ZK0Jf87YD$FaHI>?(4w&6=aO%uc z4ofz-SS@*)-GFjgwG+EBVPdN8!6Bevp;66SV-ee3HTp7`C@URzJ^1viMEW_9qj;CG zGO^Png4o%;Ki_;+5E*h(Gy{JQfj$hLIn|S7IjXZ0mRU-3wuaBfOFo3mOgqdnZ$4@b zU{3cszk5ByV)hDkhX~HRrc|)E$GjEnOxq3N^~mVhW{sOU(u5 zIIuh%@b{+pEXm9y17HY=!i$dui4Pf@kE{Tw&pKeE_u1#5o`a-19PcK0(g@YfW`D3K z`wzrm9mp>j7`+8-d%nezL^6Hh?3v51;jv5W{TM)488|+_OhvTWF#EU_L?r=5!4(A; z;ai#R0_HR2b8~*s2RtF3$d5kLY8h$>+^cQX26=^LzKdAC%+7{O1d%l=_PT+5%Xrc; zQykn0RxAQg{ri9Jbsm|1z;ABI}|O$o(a z(B2sGW2h#)sUqOd%h{83`Q7sx!(y7A4fkA{7(%^Ueqj#|L7{af(Z^BI&7 z8%|BZX+??0t*^oEI4P|AYUF^LaT0T08~<)u4Xx#Ixdw30VXoQB&J!`r*oWUEgAg!D z({s$8#6Ga|*R<)~TUy}*tA#Z37Ck?d@jnvR;O2KzsjUrBwAqfI7GjkJ*-KeQ_KoH# zovCHpxYM>7Z#Y@uh4(oG^+#!~SgCLSrVh7bh^8ZA zBl)$Z4uIH2#bp$z;xdIU{+jm=?cQZ72}XDjZO-b3YxH~h6^l5ih_^rh>#bjM6#z~+ zrGFEGg}fT%B-CJiitp?@0lwNY@;kiYu7Dr$uQvin4OPo~`!-R&FFm&iFnOuWU)$u5 zPI8-U0DLZkY60gPt$b2I@?<8-2e2%rUlL+Uo_V#fOlybucH6w)MgHVo{*TWbzhuT( ztH(8diBu_=-~wD?bFqa_F96h)zV9I|b^GfpFF-oA=Ro4YdsS)qCFJiLs-8>734@q| zY9F^5}dA zyGre?BkkZNKO<^4b^bc`G9a^`#F^7OXLwuJ2es4 zeI{;@AzGM^A6Iq8fA(%zIVp1RDFjs$zR_NiydW6D?}Y%7j(x*4iMm-TKJ2B334)== z$XWI!+L`e}8~*7Q_ZJxMUZsw;>1(X2u3Hco0<+r9QTZMP? z&?RYx{{V4ZcQ!rUSRYNXn3u6^U1rxu+USyjU4W*b*lchK#Op-PVvsHali2||pw2cN zumCVCY!Lrb0QZjV>F=HFB#W@X$KJAp#EaHmH``b&zjRv-+!0VFmXJqZdUir*lFu$< zYK%TEx4RbaItNmH+gQG*5JGwW+QTj=85#xI6XyukkZP}CcS@>jTXG_wFmXMZm_Z#| z=yKTRHH;>nV0I}4wuczuYyXzwfb`jhtGx*N-`EbRFYbM+j=(`{uGDkQJ10&YX{HIM zS-3ck%MkgyTMN<%`MGehRoSmLMGo{KJ3l=8lZ4T}ZF!QK#!d@lp21r4LQV@;YGyBy zU@+khnjOib#2UlM3FY|raG0PEzr=YShIliu9eAu&WMN8K zQKb)0DqL_k50#PbtY^2&7Q&b)8;bWv;|#Lf)Ty$mUn>kmsnb9|>(5IE3wUsEb~L*(g-J;N{Gf=v z=s_zU|LR6v1cCoUUWhLV_w;4`%G`~v+ z12I(H$-cj8A1Fgs7>xeAzh{BkZ@4Y233A8p zu+)~vVW~MBQT#Z%>p6lmdp{Lb`ubUPp-{$3m{q;dsHM7Wwn8RTUSe24=pE7ig)xUht^bLw+^ByyRBqav`5fbmAyuuRO8(NRn|(zE0Dgj;1f;c8<$J zF4;KwWS|G*w{+VLs)T0va!rC~CO*Ntjln(i=4F9Jp|G%Y(1b1;PdOPEXW|R6rL0?u z<&sacaj^~zaTL1HL_T_o(C-^h;BV7HJ*Dl2=|#a4;TJ$$(c}OC3A!JQqUGHz=X5IW zX}86Q@~m(3XDtRVbp3HdncM38Rk05!xJJdMP)TE z)m1}$7{xLJV`1C5o>`658=NhgcS&N#g{JdT?G=Ng$TF|R1w%;03m79Jww^oVVq z?C-y_Gq_4|8SK^jI0a1Ev1a!!@!nQP!{R6=P)8~h8s>16F5uBBw6~J#`8Cop;bdOy z?1VMG#<19u%YGR}*PW4SPh@OQ}X9NAOVn(Hx;nR$G}*jYjfZVMOx zGGg~|WC;#bEEBGe)k7s0@Kl+eSRvs71E2I%cq7fC5V2Eb2lKIF?Be|c!`oCOBn4K~ zBFv*@1m@3D`u78W7r*)GsKT%5rXI^sdb1JM!(h_0aIfk9_c0Y`6pXc^aK*Xyc85Pn zJ13Sp9DiljdB6MT9P6`Z6w;5*ACTpfhA8z)_6#sKaXzLCW}|ZARQNl#a2t9wc@#^+ zMsGCVYi#T^mP=#Q`Oc$%pg7W*Hp$-YHA$jTheFB;><8$LtGGjd{ z2&}{Htn))0n?%@0MXQ~J-tau3TmHq=TQ)Fj;-x7@yjvIw4ic-(?VmFymVWGE1aTF&;3qLt9qg-XHE!?7=xdHNI zFTZ2#m+;>UZ%ogHY{M;g!l6}3jSM4u3#xxPMYi*IM~j7O+H=U*(y|?hSI8i=9qz^3 z8cscJWYQ@n#E{K-{5|NC^0|kiZ4^F7T|EyCV?TGcC?qv8Q+wF|PMw_R9^&EF)a$3y zB6cC$?+4ezXOpR20vMR>Zp|`JjXh@!$uM%9lSt~D&^skzK`#eeIet!tEh~{B%Of`3 zWL7qz=WRmL(KgUb{pjXa22r23I+4+Cun&&mJ#z~Tu=CE@W(VjwQQQCX=C*`fVX)@5 z_*+gh?SeijO7W-%0=GsVR$pDTEQAWdH(l~%x;MQ>YTuoj+QN@N!p)$^IG)Lk&|RV5 z)sEPj9 zOI^fk9c-)a=Vg-4UUHxQv5{gf1Iz}ce@~v<=h3k@U(W>OKvWu^{em^CA73&Y7=92Y zJ%xjkQNre89Jn7)GMm%*fVm1{1ogZ(F4{_d{C7-Pw#%TwMJ&(zQ5BxKAk`vu$-XYV z#rN>xx~gBmKs9ku>Ks}b69zGho52k2wecZ&{});39mwV%@B5l*ZM7??T|26X6;;(# ztzRc<#HO`J5fZZ~V*k{ps->;kEn=i*P@A@>Ek=xz#EMXEoO{l_=brm-o+sb$_j#W0 z{EXN8%@lL_vLS!xV?}#9`4z*KAM7q$2Wb9Z(3$UYee9&12_nrrK|J&){nAw9YIccp zyS|jo5qDtq`xJN_;^?NLaB8YHHdW*Ed0oadY>FcXSE+w zgILIsr_{z2teQ0CI!>{fA<0>o<<5t&`aah^*Ji)~LU`Z-xnGo&>pNMxZlq|u?wz~@ z1*V&=N0yvO^o$hPY&ogh^e(B?kC_+sI%^f!-NDr4_lQ)|$2R*o3j(`124({lD+yy@ zrwnElN(Xp1^~!zI=$yt(fjWjVB!nBV#76pgl?wUGBSRJ9hU_>qdijz5+1m-qw&`BHIBGIm+aL zXoV~Jd(p*O#K}L6yS;0>)Nx3u9dr*Nat=&lQ%>#xh<5?->Bi-+^%{IG2-!LO8{N;o z?As@NTK4oX%F??-Cz3eYsf}Csx%zM;gM01D)sXe^?=SKdo!};b>#eSNYj#*(iv)P! z(mM?X0Lt6gmj*y-?{=;3Hu8_$?Q<;iHTzM2pn$ntCwDnekK6f@-G$H;@D-?R@}C1k zZQfw0kr)oqLw}R@@;ax!GW^|^OYrM_F!Q2`6YT@gOG025H-CbyA45e(nL5M`lN;Lk6^WS<%%QFBxJF_gn5j6nSoqWrbm>O6*foGNDdUf$rKK-Kk_T^Qbxt3HBaUQj!Tfr9%&< zy{XKl6iCJ|6f1dk4uop^D*}BwZ{ypy3Gy|UR4A(gMSJh*=5jwzuKH`%{#>p?v&M;_ z`m%heqRvrPjo9!dm`IJsQinj2HUEa(DI$5I@&c($`KUv}EozmSY8XEze9WAtB(Y&j zxqu6}UsSCe4F*}=b{k`=-a$g}--hSaZ}Mn6qfN1yp;QRd^v~%(egiM1>QVL{8lV$y zU76SN!FI)qjPq7+*88Oi(aLDGE0aBCUS@tQcL=NHYX%?@4`7qR5C@@RMyQ3r2ZYIC7G@nsyw zyRwaU(G|W@Flbp~oS6BQVWd_yz(R7% z8>J##!Z3=RxoIO&H^6O8G`KyI4a?^8@zV93ws@s3h8$tAfqDaPqKL(a5rj~<#Y=~X zCqvaM_M6?Kw^`JO(%4eoqAP&XLrx5BozkQNeWlPnNt>&H7lRv<6kYmBZ!h(ZQ- zJtX;_1TXFWkR${s{#|HObF4PUg=ddO<`Yo!9fp)xc89yV2m;#I9?M6-F=O8Gwe-GM z3bi)_oLu7D>oMA;7fH++NW+Q>hGF^4_t=6aW3|Z4D!YJ!1tu-O4uEF%nrgj3BHJGO z*^h5ENlG_r`qY|smA-Y#e{DS_-v&UaIb)PQ_v(7ycCPHvKH#!8B-S!G+N@$`84yNT z&}X*@At<*@-m0_~k6pidIu*f8ia5!)PDfl&YHYtyNl^7p^?zHBYnU{@NwY4G1!v+o zFUUlAkZMA&Htx}>)%t*K7CH zE_L~B>Vq4(w;5y{u136csikgt7$lMFORWws6+OnyL02y!88pXIHxak@a_st-Zq$z! zS(Ajs#ko=(2@X|;PonyV%IWpg1~}&H-fMy6>qKWs5?N26*}oqTO+a}qUp8>k%+&NVt#!R0GCC!@F_!ol+vti93UV{MFZqe z=8x}!rhg@0S8MKz%z*}%e(Z%dx1)r}>?&1#%kG+l;$5A7_AR@NOV~Bacp}?U2i%9J+2=a%G=4)>F`n-9 zaDfXR$&y`@V$IK=$hO#oY3wX>So&n%`dp}JcoM)jxO|&esJBhVvFbOu+p!#LI=`~d zqEAlboL`AtIcNM(Z1nt=XAvp0hYow0d5f>+$$Ek%i&wr%^aV$SEH2-U3`IBIiT-_VcuaLax zM2HmB0Ck%9EdU1=3>m%P*zmQ5YKY$j!6%+WkBy1SXf)}g+dCy8)AMX!W5sUA2KBgR zffOL|Io)Uh_nvp2MpiOqL||vK`Cc>)vg^vsvfUA_RHJdFTr_q;8e=k$Xwx!kt}Uhe z$}x<4-077rdfXM8>N@DJ7S?)zUi~_=c+!>PZ~wu}FCBhIvAt$Mv{7O{P0l<~(qHMU zCW)nJG``R;hWzitq;`k}IX|V>)O1k0D@-$0&b5{my12+Mv;c+7WyrutB{frPh_GK! z;gREU8#Bz0v5G9mda4jbe&&mfL=h0nvH_q0`DqPY;M_3;TA5aJY;L~pm zD!4uyu$kgqa2q2#-dGWq_P)jdrm1178xms1r%8QD8ip{(HYUJPlKk*ehjO++Gz`T| z6&e}gCnZ2AV*+!_9jv?rsUo&y0Vpi6I?r0OgE4k5n)$O1dGN9O$@@HLFgC2GyMR?Y zFs))y!e)O(1i9X&rtD;mdD5I%+y(DfIT%YbZM@}*%NUgEnY?C7m;mCkv0g;lAY6h0 zCD_81AeXm-Q0`(|>k!lAr<4A91FH&Ysj!?Juq7HuHTNw#l8?6`mFdi8e(OE1L*vWr zJu?+cGhFBjB*SA;%~cMGn|HdTb&vY$=$vWwBhKXk_Cr-L%rAqz)=84~%*E$K;<|JD ztEwpYkeZM_zjZ$)MN!|s6rcScOdd1D64&Otr2GW- zn2f6&spr&WS@c%ZNc&-o^(DKVB(H#yPth;qBhKMj))}b_=;_(*C%)&IUkWZWeoX8% z30*#SFQ7AbV5ciEmvftDian!T^Zp*uMnX^TEKX<3Ud5c|#9zFAR4>ihI7D~xlGz76 z8kGh>2d#^gU`)K&W8zW~w1x7ss?s2bVW5u6P?5EQA)|64h*Rf=&D@(3ml}H8QQqU` zZOo7vP^pB>t3D*TQ^fXI_VQ#5dxB&(QHE`F;wL-*+^Sl`$Ht8~*K`@dqP=(0j}i1| z=al8|D%tD$e43psiYG2hY4U7Bnval`N`>tx41z^v{;b12K8bD5ys^_5c1iy->4a(- zd`nGg-=tc%Z8Hb?*7I&%9GojuyC%8z3w-!=3NWiP$Pob>ZK+y>Ub*dnVOl7JYA{w~Ms(UE{Wa+}6! z{HxgqO^w%G(+}t19-XkBuB}f=vw=BBH@Dj|>)Th)!Rsrux}*#$EhbJ%QxLp#Xij44maouivqd#5o)1< zm0PP1JYX*6>o@U?iLT}@WQJJf&BQui?65n*YP& zM82ApW9FOr4Ml}p^qVlu9 z_c8obqP&t(zPh@ia%5CFY7U7kZd?l29LXBC%g>IE6SJUdEl^Pm$qEp>XEJBdP)12t zl(qocLo7>?R{_DyZv7?$Lxo|tZiI$BjY#iKfqs8{$Fb!+tMv%wxhiUb=R&SXz#nMW z8Q3QoEm@wbsKImMreGTL3}}-1$7fKwq(Z>lh@Q0tHL;t`V@;(o zXFlHupu7MUX_l3Z8#6z3q&vY!)GhUBeB>LTp=g;h_c)i(O;PQ;#72M)Y7VCjs+EPr}Pyd|_=Npt#Rf=n(~Dp*lz@nf?hSRqSzi=%^$ zTe|tSBFi*hiTps4E+TzR*_N0!!BA~nPUAv25a$~BDahwCrf`Kt^Gauxy=OYTCqOhrI;%4Iqt=h-%ikp%BaZhp z?@#}>`qld;Rc-y=NZXAXzb}sYE+nJfT%5$+LT)^~5-`>X&ZXW5kb;f_9 zL1`Z(d=fKmWLx^AyNh=6w$rd@FcrL2I?Fb0iHm+oqfaJTl~>iEhSmHe#2nkQ*r-S9 zW-%G$QBle|BcGuT16M0GH%m_zjaz^J(GVxN2S?3#rk20r6&f0a=d~|&_cVo;B`Udlth&T+cCtdHBL(; z-l{20JTAd1Mi#hy3l6Y-<}edV4G@j`wqg`guUw^@JpMBllw9|-?hu?K)gv=G^^S2t z&~HUh+jB+gu+QCRNmrFct2YVO7brZ?H)g(_8nkC(!ZGc0IovT1{ql70U+L)?sF|<$ z&b3#Amq;8(d!B1vYk5KB%Pv0q!s_U}@Rk6@}if-4ogRKFS*c}!0clpJzLXJqMuyWiq!c8&d@G_o>Q0D}`4m@MVx@n5~a zhUFU_4y9}`tIQPdaJvL!%|KKq!Xxo|gq@LEBljLswmXD`jC=gAT^Z^(hYrkD%YgT~Na9VA;Psv`zm_t(7IKGs3(ksM z+ry8LFYHc@|M!$S#ScjMYo?b~F{)hO?-AGB%U)!f5t?RmWA(=ESYO_(1*n#T)2tC+KuOW#VLj;}wSQ42zWPfyl# zzlDYOBtzyKuKzyGYvmOT7CT!cK8og?I;kjHYdL(1Q>v|>7W1~$C_mU)S)%sAYe&}d zr8(uxdgy!BZ4<6!FVpTL@s;QKa*`xn(^uX#N_Qwyuk8UPFHX8lXW9j*rw(E|_;q>R zqL>oi%$!|$sKKu;J1d&Ykvw$f^!>>1gUj}KmbF1;_M303s-eJ=XecN$Cu~uzpiko| ziKaQ@cv%-%YQCzj5h<^c$+8<0ChRAY*Bc|+Og5+_7GhRSD7%^PU%sS;_8!nU_c$o6 zj$FW&oS_d3NliQi?P_Yi3ev2W%xETMv_W(9B3JC(e*3LLeS6CHOi*sGQ2VMwy{#5hbY6O6dU&Lhtu~w%L#_WRV z3dD`)z$Rr6@ZEfc>&#r*ozSCb*s@-plLzstF6NE+S?uPjZQ?WSH%jxhCc5a1$^fdN ziE0FyTyqL(17OL$jPAVoF?8@fr^E?qzvJdxwH4pCeuzq3K-{0ps%*W()~@6gS1G(5$)9>zr3JZ^!bdtKGWxM#p+AsF}A+OkC_@ zpkEsMk))57%f-Z(m$`)K_aoDdIisyL`2l);)g=RL?N=?US$*+|Gw27K2R3KMQJzuJ zY=PX87xbhr-IWc9h-s6|GJa`dkGG~-WcoBble<#JM9+=fmyX5+q<+Egs6^uGj0>Zv zvnuGdYdgI^glOQU)@J@@N^eX;BYP^xZ!<<>^SJ{kV*$0EW8FT;9ypP|_CyC0QAW=Q zpxnC?@F<||S49ljZfr%J^yh%tja)4-7r@ct^G?o`K;0^gyMi79+fYh~BOzBjfE{697Psey=BnB%sWa;b6gYkvqwDFi2O zo~-FqYGvv69;V#u03d=;ULKw5YQ&To>EtJ#Bx%9FE+q_atVC?4yr$D?Klm7loXRZV z@h!$~w&VH;Ir5}uozc)EW5OkrZ)e5BCBWhDI~c2#S(3t9jlv+>YrhK$Hc@Tr4XymW zIQpn0tEQVJd{m(9iRV(UZWn3Ru;E|y{wME%7n|ILl&AVjNeZVQlZdm=S9s-Gh<|O( z=Uan9vtrM}v^c-b3V#Tno`L)k$s)%oRZF|x&oDRD`t#)q)XTdQ{SlkBG+FVQ)W6sn zC&xF9**Z9$+4IdS>M-tfZur{t;Lnrm`TtJLrYn~22lb1KEOjtbv-=W?CMrD+O+;7R zWtMvCvhncoqHzqL`_fAfVq)SdtvL`cyVHAI?NeSj%pJ0N37%F3e7M|4vt%lKIja#T zQr~t@u~VTa9+?LPwd?0ef~Cy9|6P_|3-zwNPv~cFbQk8+aOk8bMfpn zBLK!fu^AHl-5bwr2b3 zljm(;dPj_pPy0u@M|Dk-9B2#`-xGWX;8(B%+~f^Om-9x8DKroSMMzLkC!HU0VCk~U zl7I`S4Hmpm9)3#CJ~=+2>^Fsl`Mj8}^40E8fn>T9rz{##8RV;HU;VvQx9N}Te(^lY zV5Ifo9-~XEqe0z%bv5m(2JO>Evb7lR4G7m{A*VSkKGt#HA4Mn0pyw?b`D$6L9#WXb zaOoC*m%h~6wI2ogl~nACA2RmKW%dVmnda)Cu$?|vKJ<`to`2uX2TeKJd(I_q#^bBN_V>FW6UQ!Lsqwd3dHkYTD&VqmUC9@FZE7S0;QeErloLxJox6wHe>?IecQPKLebrl9S0m68fNS-#iTf;+C zu4Y|FXmQE9R!rS6>nv$dt^RI4C7o~DSdlzb7JMgMZ841SQyuY!4w1BCJuGF@<7fri z6(~vvsE~Mo0dJZO!A)b4N+_OzmLO}`^@V=Z;mhBKi7ZOJ`-L9J^$Ww~L1kb!^GR~b z(YU)KNrf6(q%98h(9H52vS}Fgt&3G6zhq8e7|v5Yf-Zg-;qH{0Kj8~Z6cl{v=RZC9 z-eg@&>FG?UM_F2D6*fX)po>fe8aL|m7RF1~PRGvoWu2g49xO0lkvMMgJs*@U<$YS> zT4F9yb$gk-&r_1!{AEjACeU)bfLx}=u=;J za;N4@(MAJarKz8W^d;^jvKf*QWal-SGx-DNA*`TbV@xc@kmxXkP(SY0u5iESq(*Qz z1HVAV*}~Oi`wJp7(s4*1uU$IDRCnVwCq+&uB|)J`OGJ0?eIAfT8rP?AatpRIt`6_f zaQF>hr9)0_1e26zTLbg0`7M1$l=j)sH71pv0OZtNs*#DS*B4@%>Uy#a=~=j0sbO6C zUL~xCt8nP+MUsbCq|(&frn~l9JId5Uw8)$MZ%TB|1hy>Bzt6eccj5hLe7KGs)mc() zg{U1wpSXGaC3f+X8hwd{;RNGo^#VNftasls7^}9{<-H|dWbHVnflTVGrT>1vS2}P>kfHUZR?UMg&{#0SBphWBMjT4ps^&diN*$C=DjyZOQPG`blJGWUXj7WA^BTc>Vg0 zHe%n>A=VSoF5E~EfNVR$%xc{u}+wqDb@=Un|lz6V476)wBAIIvQ4i=fl%*+#gG z@!HDwg_cx1TMSBDNy}mPx}F!_im4QS^kDCVs(n+$w=~$V3(1{dQ4Gq-I%}yf_MUp5 zmo=LJKf3Iv^Ca-?n)8~yEfba%+5fGg(lJEQ(9ZU>ro9CL53#Le@x)sN>aMNykNK1C zk+QR9`{7e*d0#5F><0V2On)}e&!40Fkw1f&;%XCbC^ZvzN$ifRl-FlXU?p>oRcJDB z>W%(XO>UM@E_|w{s>gQkE#F{&*2vn*z42Jg%6ELrR;)RKc~Q;<;n-52>15j@YyNI^ z<(`Yqqf`5#maV6QMiF0aHaBa>ZHP|cmniU~HML!mIYXDQgDKbHeqE7|lv3}h z(Sq?ZZ|Aa)OGmjAEv4UMEYbIFC~5g2C*U%Cw zesQCwHqQq6Fc7M~r*IaAp)^|Sd_3t^ffSJ#wikcqQDdHE%VwlBl$yPFug80(T2)pL z_AAb`SdUit^*_#pqy1_kUEg1zzL=jI;cA(NpMSq%ccz`Y+A#CLvPWJke>Z((bwxyN zpkMbwc4w`(16Sd>o?;a)%T|Clo_XFc)NQ2+ z%**GImjH)cYc9*wmt3d59h1DQa`%kFVDHQS#RACggOONlzB}3Ob{6Z$t%83x)G})- zne(LB2cE}j_h)Xyrw0EtSDpw5)|6Z8<}Yl_E(`ygM5Xs_YH9CuhziyVmWo#}iIc^g3!e z$wP|hfQpDplNCcp%gR^W^**uk$MfbU@<;m&CyWP>c6e(y{xY*<4j(cGXuBUER zGWly8fYZhP2mC|*Jo^bIG9A;E`(Lj_vy-E&;Y}z0Ijl#K)*4@CmMr|;0d4E_P?R#ssrGL1GV*Obtztz4(_qYG&Tbyxcc$i}YP%0(2Zu!Mmv-yE< z&JP1@d0AczrQ>`bdQPb~ek$p5v1}Yzk)1JTuTMJN0Ododl5U%#ss#vK3#NdoKK7Um zNj*?bO<@1^C*K-;KPM9;`ctEP#`l4StomV+(hv6M%xIyBsb9ki%Lxi!ebKj`5?9p2 z0&V9fVU@8H=qrFO(CNX~{3E@;5ST%GarIN(rY&2PK)vHQNptl7lFuZQ8okAKdpwmRu!{Y*5eN-YYx z$G;Z>%;WX_1VYrvwI9ypyr|f9MzlJ*32++ykY6&Z=ysd z@||!yxrQob{muoa2kr6*K#{t|^51XK9o16BKh&J4mLDUw<@=lOLIKiVOx}Jb{NL}7 zj!Z0=!bY#Ua%3?|XNDjR=l{BY|H?BQVDe9;l?Fd}z4kI^GgtBj3umh1v?0>|HID}L z$>)H{)%sgblFkiYl*j{O6K7Y9#zW0=&_PGnNT$x4RCY9yV8 zD|&HooBK22RuE7}j2WuoH$I_m!cLtO^-lSD9MjfdN z{bvN^le+?GdZgYH@HzPkfQ?Staycq_hS3y_Z~AE^_r}dm->qW59hoec-kPcF2deyO zQ2r8~reKTbn0_h5vOlidkg8k=jE@4?1kn$CuUCExPsty-2^bkR#X~DxmCGI*X-25? zYld_~m+zd~!A#Szn7QI~7i`o&CNLiVb(OKC!(nI!f{qb?w!Pid@VB%DFaqXNi zOkC@TBo6hwZk;7}l1*d{uMP*7-VxrjM%~n!8~nqs5V~6f*u(nXxIv)XnyAEHO_M$C znp{qLLRo;%k}n1m*Or2X<~L4DY{w02ek&RQ>-3e*x^|qjC#B%&%1zyB0kx`C{y6VG zLwQlNQ<^~sgFhl<2!C#hPJAL|T%3Q|1wt9-dy~exKEGBn1BE!b$e*HyNkAKPjbux^ z=u!;YPL6F^DzHxQh|RLITiK!3M?oF(UFH%IB`nA#gbh$oKahsJ(qED_ z8Qfv!3JkZDWIC$i__hy7B|JOHxwOrJw*_DCEaaT7PX5Ua_4m3xye#TuY+4;55Wd6@ z3eV0{w(}0N2p(dbHBTv5%HMnc&4WFhw9!OFQ##tfZg%Df0Uggjd>21oOM_V#?F;B} z$~#+K2L&ca&|Vt(T9OX(p9F$BT?2%-$JMp;*j|o{gYQXj_&hIM^X>d-$Ss5Nl~Hc@ zzn%Y7SpDB9<~`F_>|(74_AK7qV}L7pVYT`;}4saSrq z#4S*qrEu2i7|%qKK~T~kxaqH?@+#k6THe*e+})3KGzk4tte%!I%8F0ZgK-}Fo^IJF zoZh)9D^Dz$ZA>umCNHh(z;h%>5uK)NxqQKvZy#84^JX~m^k2nl@tDSA6&L$Ff^!O` zgO>G$Lt8ApPm`(Z6ur$ua?|TP47vE5&sDzK1*45OEaE%kTrC3XTMC=e#J;G)Q9*KN ziu`DF!wfw*dogh~*pzqKCdFu&-Z>=cPB89qs=tEc>$U7XjcD*&3T zP^8>HEcl};$iKCbHLM_vAhfTPT|OD0|10nrkyS)jz8_q#ym?5h4p<+1z#=<&e5Hj^ zOVhyhoym3vhn3FKAj^ExuSvC-cmC~URJ7d>mtro7{SZyauRdv zx;kVIyuUvqU07$`Xqzy;$O|;@?-W;R&~nNz+G3543lw&u(EnlV&u+MSioC72UP55 z6~eOmo~l&yO88M6M!era0A1~i3-)3;fE`~810dZZ1g?-3FipucRkFa3 zj)icwCOA^@D&eVh7gc8XA3;7Z1jl@3Th%j;;VxcB(nN4Hs}M)OKki$X{Nx6^$^-?Z zqBu7d4}QrQb|-TAbY{SemmndBFqm&~GZE>%n6#{9nqcUV0et_OVJ68a+@4-#FWoV@ z2K%_He+B;q6U^m4`9d*$Q~{Hgi3xxLxgAHnD`)T$K*Cm}Iq{aS#c{ZHB74nUTKBL5 zl>6L*9q@W*qQ)c>+%;rf1~|zv&{Fnc*{G{qGb>srSHEHLf(U77uSu7re0?Qo0m^%F zdsgi52AdF@4i}%h&iKT!Jzqfl%p^zNa8hgAB^N?>UT~%N>zYtJZGMMKZ`91_qhr8z zHAU^w{-O*jP+WPOebDUvlbMm_?_Sb$F^T885AavJgP6sg zUSfB>M#Ht-E2brvZ5T_d!WP5nXASC4YG1Un@4iNr@uJOC=^dO&w>{ZpW(Qv^q__<9 zc7T1}n-d~L;}%~8{>IjDEa}dTn5jhpL;PVlSzD@U26Gu161#ehW1Uy%-L$A9m=l_wtXgFq48X(Oit0szH_8pC|wMf&= zZ(YT2zUvH4r_rW7d3=KVUI*LA|K$A4y*6m-71}kz(SXgWbAE#?UaJZE8zomFO_c^>&Z2}D1|hdLMT=2?YRb<2Jq95V?sz1U5kZZA2#}k;m@`sx z?Xv$E7jEUu=eCvx#aq#DRg0|bZ}_9jEWq7qpl3Vk;x>7FLhE2b@n|J@jMUV+@V1GwiM=UUIjvlhb?|CB8%-iWOV=P$E zr6j~zKE-I=3qFpkraO(E8@Dt_>#|^Z%ZFYc-g(o`*Y)yG4V2XD_eMP60^5n0?K)fQ z#%=~EYH-2Afp|Y)eChfXTELfuB;htx@(45g7o2Lth|FFsT}ykl*|!y5w+NkYy*mpj zBE{%920LC{Go@3nNDRF7x`x-O}o;!>-mjTuGUC_-${9bA__{XY^QjE*3bI`I@Rx7 zPec)eW?scw%3cIDFvi-U2u_soTI6E46tPaRU_E|Chv!s!ODskR7C6zZcJrZ+#tMH! z+s$n~At%+2wLT&P*D;ZtHf64$h~nc7#AO#u@GGGO)<14OA{wr9b}#QRGq1*EVX!gq z8>f-2eY2Cy-!7^IK3=AG>Cc-AljB6X{+->q6h@UsshrzT>qH}6)@aLAjHn7_Xw{sCb z>~3|AXfL9sw6+B6R2HslDZ2P5S8A+ASq=y^(MH?Xj;Qa+lMA`)eD_41lB=@IuoIgj z`}b=Jw)KPdE#3KF3t3AdS>f!gC7|*i)icnQ>zFzsZ?G{%clD-zk-S3-zQ=T*6)DAz z=zO#*e5ymZ@)u>(uZTU+^&!HT1-ctomiWURfB_-N+o=M&FAI;{E(R)>zh%!QRD5$bEqJuW{0Th`i8U9-k!VFh--B#b;UC{EBLIo#XGxu4ITs z;#_nDV+GJTD%c#SnJG$gVvesiAvT#mOWwMFFw_#D&kHK|gb)UCP2t(7B1*sw{Wqc`9BvooxoInI+FWy* zZlwO@{Kx7s&VSxLZLNO*+hO}0;P52llKlqFcnF`fs(nq(DD@vVsKqB_Ng0ArZN;O$q|Z4f#Q-eD{=eeFAsC?X0NiVme~ zzx(7{N`oQ(eFjDr;?owo0O|u*c>m8T?Oxop`$YS5hwEt6Mt5w}eeEx}VxD`fWleqj z0g^cQB^GXM9$Wcg9z>XJJG*h{_S%J$ks1+Dq5EjrD>3HoaSvVP1;HDC2?rGob5FxS zb7tI)?19ASfq=E;46|*!M75NO*6kN7al?OO*Bw0f#)d7(kpa3hDD}pz<{ivR+*fyx zlN-?@u?KV$RfrY2MENLn@Da^MztT4ZBj9={RaMj~Y)gak_3Z!q0ICd;ADMof$KuYu z&$9hJOP$MRrSd$+{xr;oNor*I$eDg4#3%(&BXiA~S=DE%wJ7+KvW^FOM}NG-nF49O6HR7{VzwWc|l>H3-h;tX4BkGDG-WV!))L*4?4MHn@VW0EXI8qHni4dEV_l zb6zxl{N}e_(^_HZ_o0-x&VTnOc79oTw?62deKBDpDVh~5_n^#^>7Jv^%Rm1NQlp*7 z(|;a#$R=Yw0}r^3^E~Sss*QiOfHJ`T4y54-wNu>OFvF@f7Xay5P4{0^3#>f`|MI9|lZ>8-#cTB+deZ7a|J^ zelaVxREzf*?y00q6#nPb@PEhalNa=*vj0f_3LF70K7J+pm?~Ma?us4q-Sd#+HM5qtpHDMF<=Uj_7Q%H+pC0@GFB3at|+uwut zH@lUd>Tnyw6`7fWspsN_S9kVe<$D*YcU%j|h*Ul?f7H&!8|agz1hXZ~J7?K6tx;uW z8G>32{8MIa{xoe?=CyzLm4i>&9%0^lBUrG?Zdp9v2-$2O6(bg4bpg1kIrb%*WsB3hhUXDm2X0cJO!Ni($oC#cLii!y*A0 zBF$ILo3JjUmErFH-yrOcZF|HZD<#nxxPH$24WM%6{*k#S(Rnx@9^T+YKtJq>@b^a$ z6XYtd6{S3KyE`MfA9##V8{RHl8c*IbQJ;N;Z9d*^HmY$oH$2OJ*x}7);`?UO4>;*6 zT!bL?k)G04rr;{vP^GD)DLt(xs}H_Rc9YPLzT8gVX%g!nwcUG;-_IGcc0(td#-PPN zt@f1DcGQ@Dk?nfR@d!T?y3-m1jVatr2FlrTM>1(Pf6cgIFy6_oYmI+!!xO#odL^*O zcX0n}6|!Oh$N%XTLm`1=3Dt(~YFKqo6ozh3RWxrmF^Ke7^4x~V#Y<~_Qu9!O7@u9zL zT3i1((KaV4Hhtg@p~~)Bxh!Y*yg(g%m#dOzxB{Q(@${cR(cUv^->Lf=08K-wH{C2d z*SyNKd8CbtQyfz<_(VIw>0YGt`09Zw(xo%f^a_`d+s&+_z7g7&!%=y#=p385JZ}t~ zQH9~tt;uKfepUm}kH?_r6aQmrdlF7xs`!uOIFoK)&62?_MU5vdTlLdkE3aHl8+mS@ ztIiHoW<@>(pFhXRkfKMq?p!R$TwhIN?y zT4X$sr6UL?@(|Qj} z4HrZk4ItontIlld{ew2^7e61bke#*?-aFP?*3dmu+N%LKR{uHlH|=fig5^&+ zuImd%sf6VDbL5zF@xm+d)%4fw%sbbX8C5XJpew-3I3e+!NdkJC zhbPdShbP@9COYnxxqb1)gLzZ8S(GTJ`M=i~*>q0`x1z%m1fcFbcZ35|Za8ZF{=}~u zM=W`T8b)u#sh>>#-TR=c`@zhMa(q%&!y~uaQ1|}A6L~o%Za}iz@2R~E*<{NlOM6`K zXCch9Tp{O^MTV-;-fK*p7E?bTaiolM+lVs%9sNuY^{##8Ade;G%Igfg2Os=%V^FOO z?ZDZ-J0l-9sqxO=sg$4(D7dQBog-ew6h?U;QJ4DW=Hnl-9Ml>5OpL2J&du-L?=xS) zIqonJVWxH25PL)p7G^i42E;Kao<5)RI}29w>D_^QL3wqPFUq}MmzcHJU-lie)4OQ8 zb)kYOl;o1Qmo0Cvw$-1%arArhe{OW!R3fP>#3u*iTYp3ap1iy}!^6hJaJE@`_hGfh z&?kKA&1PLlnRL!;A7~^`d=rG&BK%NvStWx)wl27>ttlSA9ccwC>Qkwpr)2$gdQ>jAc7vndiMoz%V^#u zI3c$3es*#QyG?SdmhHd+A!es-V69N$pK6(=QcDTT>m$lqgJIsUk>ryvagOr@kBa{| zhy1twef1 zTS8IPUR7I+qQj=P7$GQ%YKay>G@@1zdp+^J@B4Y~@9+83!QsP^bSyCvaM06!|1Gk+I7C^g?})Ml)LXl`n$>nZ5few z=Hbi0W6j9(mT7L8H|;!57UCVuMFEc_t$Cq*z4$LtPBaYI2(EOx{*`@wEcd*Q-U@Oy+b?Pm(Pa9i1!gg1Xy2+XNWW?+|6eCitm zYuKNU1p-)|@B^+%sVtqR_9ZvGQ$WlQ;_V$DLSe~2Jzf&M;bfz*2_T+xziV^FKap)K zxax&kOj1@2)=2gh$fSO=p}_^&9yzbvhvGg$co%Y+Ep2@iH%y8{9Ew<~%zRD0Z(R5J zPSYfhMlU)UD}!(tFz#Xxx4`Hh6{$OYq!E^{mv5qyZ^y_@`#jaZ^?@>g>nCcYn#P<=MZjgNK7i$B4Z6fX-^pn#*6&F$)(>EAFBJbg5zVWMQ88Lv@Qp~m_h+=(swbqbK7fm}czpl7NM6*wJ5IK<%2_<^*&# z_4XsJbpYt)8Qj23L!1+4`_tPSJIMj^wqM78hIeVz9BfP#BbyceSQ6FSOz(+5g#D>< z>c(>V|aBaN6TQJn;HXE3@Y!$bvXDw<1 zeCkV@7qcd55R$()OU~)^NUtDkSkjhgKJ&j?VfFe0VH>~SE$%MA^|A`c>>P1?Pj&7V zE!wBujmO#V>VdJGm=;qn#`X%E|1nrvbl%8ryO&8ol`^IWjfF?|@P@8WkS0X$IS^nc z&11-JdeS53&9Cn{B_9E(l{N<*Z-;|nlZHDM%+l0+P%p8EvySVf$!eduWzR<-05j4F zG>|zRxAW>tPN8AV8fu%MkvrEn7Jj}fPuY0;UfOYB_(5Qq%e@ouX+QX#Guy?f0C0$vDLOfbJf zncMx2$V~l1f^E)NPBIJAi39)KmNi$oODFU<4Dyqq*x}*Lk5?L_%5OYEmxu?vO59T8 zRB(KNPx@42V{uwEG<&?&f=!MV0fAWF&`BP&4?t0moECZ*vU~Nt%kXlA4nw}+=Z+O4 zvKY;SS>Mk~JuLy_BPpWMH3ho;9R+=8j_8_T7Ee;0R5>fdIE7<8772kTCYh>r<_k77?8`CS&%S zwjC}(NVD~@lBoPuNt;*!s}Iw3&(Mop&eb`SrkFs)Zh=X|jNyR$*wA$L`fKj-^}LQz zQ)Spj(NdigveBk8ehT{19?U8gZK~_A&U(K9X0;`Q~${7ndfToTAIP`yJ zq^{qyn6w)7P~t9ullebQ-*tz6J>a@jM*qLF@-q{PXCV=7a(31uaV?HH%5#2ZM?1Is zqk6SuSKsam#Bx7Q_O8B`=#bo>_;eh7KE@LvveN2^*849Rb^L}!5XH0Oo0w>btM}N# z?&o1@Am#3$V9L9(hic{jv5b#W$-_&5r-% zTL7Td-H3oD<}zOT?VQ<{$NTmFy%+y)$@n?N^N2PV-ANj^|EZr{p5okC$+`}WvJ?}x zP_O>~PyCPOEJyKdSX%%2gTyn##8~(X%Q-|4YQhneD(_hxtb=SgF#n1j>>+QfNaM;; zPcO)GoS96_9B>(91Clw7N#RP=?X7wwxpqG01>8R#;1ivFf2Y}%dTzWK@NNp6xM z79k+RL1xDCk?A9ep#UlH{J`@|B@kG#bBShr(i|Kt9r>+Me_2K6fx{{H2uTc_KKs6P zYTP`b$7S@i$jc5Pu?hl`yaSW)^Tv}T%wf`Iv^3OyMbroPdtpJ=!*0(KP1>UyNaF2D zyoTy8lHgfoRqv#KWW7$#s4+eV-5s3^KVJ!dFMIj)z?!hVFZ5f!W&XumvGT!l;TLC= zkeyHxWq9&c%VLbl2}j|j&bsA6Qo+)*-AQW790`DX%CVEUd#R7I+4z~R1VZf%#`b@^ z)p&I7P35gl7m^mc@xYMXxvj8fG-c<3{3G|UAN%;ylz>a@uew_Kk*la$Q$MR!<)aTj z8u&;-{<%zZTUAF-C|SXrQ-+a9K|tC5fv+W$gO{+9#s%CZAQ=PMz|ieP&m}hoHGI*g z3uPc2QcEb8QacEkT z#5s6T`IV$+yMVF#qs?tYmD>$LH?zNKKWL=DuGMsfMcYt*%je;y{*ayaM#9O_k4}Oz z#_7BAlGGdfoR!V?tTcP1jvsGat}OHAvjgLI(@s4QN4Pnw`JMOoPZ}+H zYGpjy{+a>9hR)dEr7l$dEJ=A>b=X>Cb zuP4pnC(Szu%*z51Q~3KD*RA$#6j%G@Q9yX;>!V(KabuRuz3)jB#GPSjY1(p%J8U1l zzOt9vnYQ5RvtBjNHL-l0q=VeV9DpzOz;n}G=X;x%r0hESHf&zgv?ZkA6VZ03JBIl) z6B|G~0tr}K>YqK04D&OL>}6#OjR^Vks@We*B2GWtX&@P5mWJg#<5l8-mJdgB-aD5WD<+!y{5NLbw)69e={GNydHRipmG9cJG zWVNh6HdR;Z(Gj&z`@wwZa27_Nc_=%)mSB;hc)jz>4^290`aII%Le+fk>)~QEQj!U= zKq-A^zcw}dhgpISF=JHvNyJOpfBC-O#aiiManzO|-oL6ge_#i5?XdlsfniEF2W}o` zUs8z6Y&+ODM`EXs%cI3!gi%2GbPygu()pLjSL6m9+LU)F>aZj=Qw-phOO{zvx~4>R z_!JQ>9Xy~uzuky=J#EeaN-4oVDMFCYF;YOAI{~_aV0uk+FwB2w1)qoB`8|zjT3!tU z^D@#|><_QZXrwV#CmNL(HJo{pF1ScbjRynuzpg7xl!ih+@4%0j&Z0dMe@ZRTisp}t z6$)|CPY`-7Pf48aeE)+W^l5<8cNJ{nj@3u*xCPPTqi^Pj$LSMD2K7pnX#0bdwq$_r zF-hW{>RdJQ4Eb$*4_4z~SuAnql4QAIOQP0iWf~p!|HB=x)qn%&guO`@93I7)9c-bc zp zCL=^{;=g{$^PW&@Zm)Z)g)q{cQs&b0VkIzf-@ehJW-$*_)*k=4mGvXEzMqp;WzG`s zyd60|;npGHG_0B)EsJKe~fAqQdKvY@QKKet$%%A;IFFs_cD$8bBI7Ib`-4Io`HmiO9 zQ+?^JVB4#vney2vR(eVjh(7D#w)jA)nUf&=Zv%MmvpI`8c3{S1#O+$1Vbz0zC%IJ@ z);CGo#@FO8<)YhpSVVEtPeb+^cHW(|L{5r*mztc zU070MSFDnmE#|Vp`A?jKencpT_gQ8ai6zJadG?lTM^r|TO`#V!%VJE= z5<^-kES+pbU)F-Yl;m4~2VR_d9P!h_tI}?EDuB=_7yg2vwzY`!ao>xE*|n3HPQB7t z91ymcHzF8NVqf)j>bbppW6W7^Y*-#}EzC~1T8BHni@eqlcui5g)qX*&p_7i4?vTW^jk2WZxZ6uGWl$G*Lh(%Ev zcF~&)uikAsX(AoplZR~^d+gi^u-EF=BM+7R||;j7mA*(;R}C(BYH_ZO|I65YnWK+qijs}eC!KC`U>xXDu>1F zO+n_p02P+X!o6D-Eff?)P69d8z@p;k-m42vhNPh&Gp>8GbdkM5{#pE1v1Jv>$>yX#CvLkP zpVKGJC@PlfWg0@sD7cW%En3&c+7p!~F2(F~EyfHRB%}g}sz2NR!v^AyXdoZ_$P#u9 z{W*xc&c_*J8hfWhrSUHi-06GmeZ)Vc49g$Y;-wpd^G|fgwQEVNG6LU<=yA1yzFTGg?q&4q^HsfJ@<=;S06yDeycLwUI-p_Z3Fd` zwa8IPBn_{6d!-u?X%e|BV?SsFm_?amN2W%o&54#rQN?|u+b=qPa7B#OsaeqXGL^RJ zzg(#9ah(->78#dmxm!xokgYU&q-{r559MJ$h*?*Vy(fOpnW}25@(!?CfKtvm(k+}- zeD$PBn@3jpxnafv?oc#$RX${~k5}TEimkknpqau#p5g87zC0FFzwXzG5~&vcWr0pC zNt?w<*YL@&t9x>IN!D<#Kc=_UkV>mg3N4(1tjc&1i9}IXEk;8{Qh#fBt=Xs&xqOgL z7yB*ebT#(m-bBwxavMhIk=WIsAv^OF%xw}sG?n}JQ$hLwF^f`dQ!;8|w;@@CX#e$# zo7at4VFbObL>|Aj4ETWNUN~&Yw2Q6AUg&DX*GSf+iNgq$XDgcR0`$MoSDzmWHl|9# zq69k#a4UkM6vL@8G3g?>L%*%SLRlC zt?5Z!8GR#hEW`wpICKpFBJEf@*`t*2d^slEW* zMIMMH_p%WLiE)WDWhzy62MG2LS9(JG+!1^p4jm_iisIl^{-M!xZq`%jb~e5vrrlwB zVijA<1Os*O)sQ``s34mxqh*?8D9#M6bn>G8G#s!`16KGQmk!6rDj)o8b6Y=h$9w%$ z%+-CQOS&TviH~gSp+}gyr&7<|oII3oj0L1hgLTFh78T(p5Il#5LiUa@AvUVx!HAQn zj0m{j1pa-RxYwDi+WhghkYIBbEuJ!rSGM(naR``KvbgL4mmAl-~s%V{lE;{Y(&YILdS2|oTCrc;)%dgUVr}9!e&Jh%5 z(iza_fKxJ|s4xKr>RZCpSrhL-UnnGei zU~PN2p5Z3orGB0i)V_G6YR|YeC+H*4mK-x;W|vJ8o49X_LUfantcEI9jruVoScB;C zntMrax}@>6HvncmIeO{uRyg;$fjy-8GWH_hjY7FUCv1Wu)WYkV%pad&NrP^tj`CMz zc|5m6oTai$gwty~59ZsM@iI058oCO2Scq5=$v2VyXy-Mik@Bu>CG>bUpUP7%UK%MU z)n=W+PdXlfB@|wCu=oQ^1ofXRkLw>JZP7RX+(IX5uL%9H1w}l>k*TQ5QO^%*jmM6r zYk9l4A9M?_x-2+IW#_FvwWbBru}kybAPo<_BfWBpNyHrVSJnN%W$O*-w6T>c_| zP8ogrbpQp=CX&zQ9mUI_r$6C-KlJ;-Bp2dbCh?9_V&{$Pj9^lQ%`i8V)?HAn7nuVo zv~(#Nqn}xD2EFlY=(d+l|Fi?VaSBnQouX4|Nf}e^jBl&J3zHA^pqI2Mrcw_H8 zyJ%sMLosdndf~=m*7XN+Zg5KHhG6Fg`&gza4d+kcRT*>5K;+992_K56viA z=veEqg)`Gj+16C*DIJ-PiN7B*=t~f1e!(TgooX z;0o6}uzUF3-C}*}@IlM>vPF{MT;WE1TNs=o!QAfy*eV@UY^<9mP3^VJ1=Vh{OWg^$6medSBv@*wb&Q6W=?m%FZNySvBEA?QQRe^XLN-JPA_}G`o(+_>8@Rfx{}2$_h>r^ z4lQ*EDa&97tNg64@ZUTCXYqCUj`*0$clf# z+S!~+vu_e6s6h3QJTF1tvxS~Q*mm+kuTs(%m%cmz;DFj>h~uIN?2*od7}z#IP#}oO z$*TsU3QF~Uwf7}nTd_aBw=g^*ruIM?Npjqye>QO_<;e~cuTAF7Lh@JK-+886Jk+s& zt#`~t$-`40ojZdKHj??Sbe+`|4t9#63KMkVjE&Ep40q-wJ z?7N7R&2F}2T=ClSzlO!?1QQ>=ZGCsAiXE0FnOF4TVxEiAj%jZh`p6Pig0w19u!Pnn z(uX9y%sTN^i4{~NG+>ceWt=)e$>B<9yz@2HWLZ0`Zw@LZ3cYsz_9~%lEa2J%Kf^>| z)6l*kL)}g9cZFh0x0~wBVff9)pt#|idA`>^`IFf~r6tB6B+Cc@+lp(WsWp`t!p;WN zffY3>q8>rDlxIgA0gk5R@97>NUp4c72XruS`j*@`n%mTkAdU4SED=MWQ^g>TLiu;8 z%O0fh(*P2q>;KT&-QX^v0HxZkbJKx}i=19x8_~u-seG>5i=ws6oJy3W{gn3xQ=N3e zGKA8NqISd`PVno$ctkWtYWg@vq)1Q-{wElxx}rj4pg2`%MALP7AdYp;fHF zW~B?C_gUAL7|;#;2KH%0N`xJ;wBdo!8IE&w6cyv{%lQTukvqH6NLIRIf*5(_8z2+; zJ{cRW@SOcHlmk4-M~HT}6X4=%O1)Al97XZC7o)9SC39y4&{~IOD2MeD29gC!za6;sToTHWRjsV0vOOH>VAWC+~UgW>rhFJqKRxM z?M2z6#dbntc5eUmXx297RX-3`bZB6}Z(V7L{DLl|zC;}w(L!S~O%UVl)?43xRMZBa zJIw zY{Z?E_@U=;!p*u2kIC?Q?uyo|f;# zg;17WyVPY*?zlYzBR<@Giy5pz6*up>(ilc{OLtr-1g3a1mOAA#iJ~0;K7HiKotw#J ztP_D;VAl||XMW~ZN%#C_x`j_8ufIHqz3amMu$51=$6nr|hfUL)5s7`IA{Ud)Aif^S zxx&`?u$O}JOZsF}cEi?rcEwhwclDw=iZ4c+SM<0IH{YtzlaYZ%Qz$2!WBXL(#f!O% z$)5iB7UAz96(jmE0=va{m%XH;a;1qipRj0~QN?P`({Inm z2sGk7ufrz9^egNfqTylsJFBg~mRz|`>_Iq)_Bq!iy6&asDti6hwVAl5PWAMvNFg@u z!*_&QVf`|S+sX-M6U0*W^k+?C921E{N<%S>veF|2+*Ntdw^c6g?S$sp5%HtP)-C3b zdD#y!qQmZNN5Q<)yRMio134RewZTH$?y-gX3d~mVTow;cxzpCMo=rz67##3`Fj_Gv z18gsAUN3+dkW6n0Rg~rD1CH#^<{2+!D#uh#Ikd07xcUxe(HLbqet$a2WO`9#rfiz& zk-nOyh*=vW7Ga^nz;paSLwJ5YD!)sG%|>0%*-Z=lkN@Wx%* zCD!$Rnwn8y;y%0Qi}P5|WmA0ppcXG@+f30#9si0N&rK^*^--k?*WVL6CP`q5+vF)I z!-46#Ux76tQi+S!Au2{xF0ucm>|=01_f|&UG$+UQu9FDWpm5CJx5WGc9q_=Aq)tc@ z6}HH%hBhQT!^+Zq-g6b(>>;3FfmBxGYfjfM0n) z{X||VRClGZ)w28x$yc6u->!%LNrtb+vk3|vhS*Ztif&RIFtI>ny0eKKqBvuEXDInz zpNfzJll+0v4+r7juxlwu=XbbU*(|1MUi7(SSw)hI@i<-v3~W;2l*`0RDNKO@<%X3D zIn(>lBaqs>$w>r5Dv(d@GzHY5#0V^xR^PRuDHPA1L7dM|TOVY9;J`%6svRDA z=dv%}R9owctvh67%#JT3re4IUBuGzlf3lUkb9)xVx)~kHJdLj{a^S`Ox(=^C@f;~C z-c-iajK|2t@KfFUqi_G13hFGb|95qriW(~aEw#mbns!8rVH;2SYTDrh~g+#%C zrQ-J!A2f`5!`IrK`D-83J^5ymrdrMjT_6|c|MhL{%))ggANpRmw&@>=pZ)!qfR|IG zPv;Wn5?w4)4HgLMy(j!3Y`&k}AN)PBN6wviM~2;1;Y_vKmFrxEg)--jRIX~}sgD;o zEvEu8W)TFgQDy&LRKtPP{&KXo2HgB=iVb0`T9(LmN*o`u{C6waD)PnNrPW*yEez(K8rU13Lr_qq)GnaUnkpS3{-u!OH6nT?ocVEb zM|AZG_q#XCH5bP<5MnVe*-J4-4Ose4c zlg$e3%^FudtoLoRIAOBrJ4Lz=9v+X+&7yH1al9$qBe8pooG~+M8u%n#jet@RrUO)nJRBN-CfK|fFgT$EL__J9KpHs$-nyAqq8BEYZ)N_;Yw*o zZpVS$84o%&=e5{oz)>4;=?2f<8EpV~=k!>ITaMv~AP8V&gJ0C=wYtyL-$s%wK`&8R zBYM*cTe~cD)NROQbUu5z%2{65QC@vR?rO~n`#vwAUu`R472C*pq0zK5zXDGj&N|)Z zyYRMb{4_wqUoduQgBy55MynU$D>4kvvl!{ff>O(LOOD=}p%{2IYPg<{!~)O%jgblr zAW-YgJV*MuojE20YV{ZK^mY9XWzs)-_~zeF16pLqVV+5N$6NACsSbs^xaqG<3y*nqHlypqZq>o zQDt^X=&I}~IA0YP$2dfb=15l z1s<2yTe;iE%>7^qrs^2#c2m7_F7dPWW}SS%$2~3euyv2&uGQE%Fr0z0bq)wW?BY6>2iLBbU(YG2M&O-Cez0S=# zCF}C^1H7T`VlSDM{2zY;f(BF?1gL3_=_?b{uFB~d>-qAvDJaOi6nDpI$*U=#253}c zT{#h|Mhw3;1t2k03f|l}+!YmP1#b&7S_t;5$UU|1w}?}I>`}F1x5*jeV>5pp`;~ioXiP9( zz&1a55V^y>_~k=bFCHd~WmgN3b{@ zU->fz+w}d*o}KyQ&ZF(9rV+-x(hENYP5@roGNVo~&Yccu-;BD6U!^;01#OpPr;X@3 z?rtL6`Dx@kdIcjtAyC@%9r&VPqP<`Q2duGPJk9jY-M511(CV7VZe+)^36iya?7dF( z#ssi2@lT}3nY=ST*3H=BpX~Npm|q%g{xN4*X$@tb{dFn>s)Xx@BGN<^k+HCsg@Z8 z&FZEt9jaRq6fxl9^eqgp)>%5Re4_&^yFRM2`70LoyR*&(6u!aE6M`>((VYJ(TN?GY zwfqczRwOH#pI-MS{Gew`AM`_74EN>Ok7vXES159cxr?8wTc_^I7nR^Bs(O(?63WHp z+Wg*M^*&2%PcmctLDE`?gkKy8w=jVAzjP=!YSjhac{uK>fV~2 zaJzZ;xgC{x_DhmZCq8M;o!)_$uIzyDLtbZSbyWyj->ZJ20ifLyXvI@7&OjI z-|0T1-CxbK{-jGEMt?;Rypyn-OO`!4E}rnrhkL&a-v0ojUP-zGO9d!_H9evG@173y~0(+$g3-*78xICBHztD;6FS zx|8hZlMHa{G4EN)Equ^8P#;q@;e-#|4#-^RWG0p}T>yjvN+v8QFqLsBT zH*Sm9jJ}{`ukzC2(u4?}gGi`FM+28KMqA`lFtJ5hxklUb-vQCrlOn927qiG|h_+_t z2sRu`>Zi^&>*~H@12uWC$lDt*(^WG0wT0KOj-{!Tpy141;M?gz%(Jc4_qziqf0eGd zyIqh>f2{QPL0<=Ck!=6CR`Ohc(?(Ez^yaHD3h<`D$vW7cl6qRmR;JXPI0D_wE2j)w z<{n$~USNq%*6n@pXhysp1>(ED9##0`IxQUFcil6Y=B~uy_FL^k=R7Zm2GU@ilTwXD ztJIFC6c)|2xMVj`Vv5&Lac+g}-w}lP@F5A+q6AIxDYWg}AeG7dBuJJkU{wt;n52pP zcE1c;vD9>M$mGpRwdB}nSmEa4KZ^8iA>49Mc86J$o#8|nRVi}_GZQBB_bRxPK*~8M zDinF;SsaYMcby4+$|Yv$0I(#00heDcNy6)q4D-Z50S7?#sI=S13X1%S(cyqrbw54 z0x@BJl^Wy9#e=0M4Di(oI_}N|l~LE-aI{aAO0Pk9)oOcO^Ue;#-ts(UnEOQybkD8c zg&6CNa~%fjL?zYP$4IyFi(tn>1#Sh9lX^fE0d7jI1Kba;{*u5=D#tCFi${j{;Xqu)d!^afg5S0e&?w1AFdWj&0Z zem`~*dW_p-;UNMty}e^g5158#C)E1Q6tuV*@aB)ydEc1OGd|;`d={bysV}5`7fD`l z(4gy=X{H<_9W2A98VoO_eHdv1F232Acx(cQXFWBDpP>mG>TkjQAaP)6X6a0#ZxnpE zh-a4@sJVy`uuKz9gxbCZ&`i0TjXT20O1+0B>ZH^ehTlty>#K6H;hC#QPW$lZE>Ss- zkE;L7HPQ_$v4qIn%I9fyps(&3fq39s!)i+)wN`C@uEnag_DC|Nxn6nI2aoqMU;phD z+w2ja#hZXj8|)#(Ge^D?i;3{;R^?M|5(lNopw{_cFLPG67XDk<)1PE~W> zI8fY3U&yi*3s}`wP_P=jbxWgPab1iUNqF$3L zwdB&X5zh)!hpKxHu}kb>+tjzz>C-*nV!~D-LtR)Sv-Je>bi?D&-wO%dc+$0*G=_m3 z5y!}*7d~!w0U0~o2bUTgeO>YEYr#hY&vqJYPT%Wfk2z^Cvts)+{1S|d*v^(xy$PJ82Z1B91Ag%1pB3@oezyE1ld&V+>qe-mm2Zuu=8LOb!86aGUnGG&v4aBm zs#$VPUZ)Z?)}${aPLc1^-YA4&8X5C4POjNxP(GpY&g@OGYxvp!Vinod@vQ9_xM9mv_jVd zwq?nn+l&^mfVPzRx~uz?w+`A+7xho&NRmP#%XG5(W{Z!cUrJ^F_3dx$E-%J>?)m<9 zZCZE$NfKaKMVjmmS2y@7)SJLa67=@<;}FBi+X0|;LFKR|k_A&LV5_Be?0fGoUyDJ- zd_Nj)^wbY6iNsT_{DzzN%AuTdT6}-r@uAsC*tF}L4s_tN%OudU>*n83ya~Rmu2(AY z=JeV&DMi*T^uG-4LdwLwzXTyrye2Jsg(a8hib&eyN}BJ(;fT*Kgi-&llP}V!DrjXl z{4%Mv3rZYFQ=s)E*^`D(a4(9;+FyS_%ZO6<*{OK+=!ZucdZwJEgN%HH zwjv&2(72em<&O7S_$!7B4^hO|uO8akeS>c+&nP9m0$@2asto{W7EJoy9Cti0fZmth z;&`XjDf`t^p!k8y>#JSedTTh?DVOI^xABZ6rsOFMjB(qSjF2-v9Gy+!?k5lF!9-(z zQEzyWAV$Kl(wgIitoh|~Y0cgWLX?EgtH{t>sTf{d9kVK{_Fy*eDiT(i@TzX`c{c;#S5AWAg6q$gUcB;x#s|FMJCbctiq)ADe% zFE);zpk2|r_;5NYNu;rk@mNpfP^S6d@M=g5quMZVe6=#8c%^zmoiShtMB(X9F0#at zi)Rj1QU(rf+A~0=?JM32G!P1i9GXsewK-H&uwATei*EjkFS@*rs0g%*qofIn-#|HF z+~Y8DRLc9wo^o$E&I(iXOb!+Na?E8j^VVu_g`!zH2sqwT37$jSHu6x|&^u|tW1pF(o1M=w@%lX-SJ*6XU^vMH1Bp+cbSShaLjSK?*1PGb(~ch)k=`-S?sKsXN4*{L$^`%5o+6>BC*1fYfD0r*@J&U^$iVF=q#5j=bRB(o zRqfbw4RupuMQ#3bNY}lGWA|fYS2;9O`&L0?F?)H;vE#l66Af+XZ&=!8GM=2IFA)5` zO!YQMQETKrV<^`zHfN?UGw$+^BB?1Q98FA8E&tTPhdVDKJNW@G&rMqlPk7Id z6d{GwHwZFtklV=KKx8IdIZ0#(Nv}~R6~&zY0}b>=Ae`imrGAEo8A?3=cr+slSmf`4 zr%-|G#UcJq3qksOa~NSc`Uq>)e{ z8+#DZHg5QJ`#&*^?tjUzd(m`V(wcRXEAJw0u73Gf^Ega`)9_VW=84#uVjb? z`OU*GUB_Q`${q*YfbxD5=YTVf`wKZ%mJm# z4LX2ZitFsZb7PIME&o@d;wcM>qEr1j-Ocav*SIN=0Zx|bUqCkpS3dk^dt5p3Z#?_| z4U%=gjJhZO$ehbmN$-CI`2O={_vfU07v{gKerqWH`yWY@|NY~3EILIjAHNiha}NFQ zAPT{cVhPgY(Y9K?^C!h*Yoc0#;+anOmh!y!0MB31Chi>1{}atE45s+qd8aDIrL%R&&#Jib&l{o9`p-P)Hoy0tXj_P<_==z9xU=RV^& zHPTfiVsFZ7*mGN7iIl52uV`P0q*qG@*Nz%3v>h%bw3Ct;?G$AC@lL($VeV=d=uST1 zIf-j9^B>Sl1VOuxR3SP&{BXtd9Oqe`YDg2a0uK)hBw2L>xZG>C4*G7H_<>2j>S4y2 zQnB#%GH>aw)16CWz6ACm!=#Oe}<9xiEKt5}VC;wa3O^ekY|T5bc+so#y%w z*zsxwTX3;}a+?ncS-~89WSJJr7uGU9)ofc~J3r*#r+%eX?Ir;r zKLe&av9L}P@VB4jQ>Vk`-YUeL?>8IVAHxkQ$DwL;}eja6s}+iHYuV&XzHIzDkh@-!aW*!kZcdb=x7UTAFM1 zKwio;lm>SC6@2nFSZtnI{PgUy2XhHx7Y_oYdi;p4Y|q%a$4atj2M%xba;{W)!rbas zG?D?z^FfY*B>F}Gn{AN&_B8Q~#2FE<$5u#QsZ5Mv4e(xy6J=F943y3IK2i3xmDHO_ z-XFfL=)eS4Wt))_Vui94EPyUO|7?G9rJoO3tRpT%N7h5;h+UEcr^_T^?$q+|2||0m zs3z$7fQPqPr87LwyU_F$Nj-1)dZ&+a*lZ|?CKbw&h55o(KTgW* zOtW%#q=@XYlY1@-_M-<4Hr^wz83iSM2W#kfK3CzgfJ+)M{^|~&1yaIqKoeD3 zvm@vG!~gJQj_s}v?|tb*@9es#UsEXu4Qn_l3QsBKSm`w*o8@}OF4$A5YmN3PQ$BjX zgxX83yhr{Uo*%R*4V>wkFvl**60ac?o$a;#xFipYAzo>MKST5w2l6nu0 zgN=@Sz+0^t6NM{T8MxXzv>^>U_~GIONv0>!liLG);QX`$Uu*~AKFvG$c5xQ=vBt7q zEBl?1*#Xxgb4*)nR>t8}GvX;B$$LWl(O;BGBE5WvKAM^1n!qmAtzDn-$c6*vcT7%> zkG{_i-miVbVfmok4i&NDY<+;tlIT#9I#xYvYh{&tMog^>Rej&qQz)TArGPJ$jvDP~ z9kcKw#Bh1sVPGP0$7oK1ki( zzNxQ#ugbNX+%mNaagMz9Du?PI?MpvdG}W05pmn0*H_mNld79~&+A@FIeUNm_kAHE<$2!^u}cw62gG^$IebgQ+YfG% zUMt;{I6&^)vl93E3j_iG4{NpqiN13kK13bvr2*NKlSy`Py%kMNY(7_Iq-b-JC)XW#)GRYw;;Q2bPMD z)8BGA+ek~Vo2u7#LvB;8h`6R(7mN2lYv;4-XX(xtKRW~*^eYVxedMF-ZWd1#GEaJ& zUOXjD46gIsIU79tW2I(>CvUUK!y5 zDAc5N|D;>SKUm^a--;Er+RQv==n4%}IwaS!q=e$F|7gj@*BsiORFrZW{d}K)&>2Tg zDw%!Lmv{XYditxA?TyH$;iJElP!XlK!JK{lY^)gbL1EFK+MS=4zTfsd^JRPI_g=n$ z3Rms$%~t!jH<6`{)F#~(Em<FH^qa~S zVp7-cDZ{8)dj4E{u^J71i~t3jk4CAfWB1i3MxvlDOOf1voe`xLQMBq!P3F$AoX6>z z+Rfi?t~g8{h~JnWL;=uG#ip7E<6tN=DI;&TVCcDljiyD{RsQtH}ufV1Ot zwkGpwA?_UF|T7E_K~3kUL&3oK~|rbB{9O;4oF3s(OS+dGd? z<~jh&nBqGq7j_Oeh>54mCzHc3h}*2oJ~%9Ch|a*M_4{5GL|mf3@}iD8P} z?!oI)`+9qUbwM&r$C}ueUY`thuJ@$Cj;0$U($5iVe;nSFyky<%1S*`R%kKTUPES1f z*5lMlbmt@Xn%W<2D!JI4X@ZtfujcktY~SVUST$ZPPmZHK%rJ4`!_t|YIoPNL)qH7K zC?GgZq<^%Geo+Oe%Vpx?%JwX$aK(R7RNVM1-L|GZpbH#lRF)LA!f&>5$A5W`Ph=T;7EuAPz&+*EIL7&@Cdk7UG{ zjkC*>*LJKnjYXvtUjF?1^{7(mXUoU@GnMBIx3(fn+%DY`q5JYKQ#f+nnjM1=T=1|c zpwMwFVW4XWe8iQdDKc4w)jWijn?jhc5QG)XC2quhQ7^faP%g{48lkyX{aD8IcJnPy zai+-ord+Cb2DKD}fBnk4=Hr|3n8Y<2uCK25jE1I=`1%IE^^k{#i>AuOt%Wku&aW>a zU54lADx@4m!BcZdS1Sd0bU7y)D7#U%5E@A(l>AC_BSX)DRw@SNlT2aXW>#_?wZXh;T)|#qh6pzqXOXiET`)->bhO)oKupoy%O8w^7-$)?d_>{y77w6z0A62 zm@y@MfD}3Ln#V2CflvOu=>A0cm;$;vo}T%>n;-`**x~-`%fD2hrYF?KBLyFEZ*<@+ z?HODk_aD82TkA8LkmGc-;TdFoB)9Dl>ynnLsH;3{Ekv8uj$MmTZco$$2Uq81WbfiD z;=BdKW^sDm1!?TJ5BZ`o~S^b)*=h zFR-~wl>;JE^33%2<^>Q3WHpm^2Vv-nbY=bxOAAWH@HD6(n&5?ZVrVC(m;KCj6l6ibN^x!`Gc?Q_2%7rC? z7qr9F<8qKu^qKkh)WVp;LNyzDaNBFqi%%g6^!~Toh8L0}Lv0xQ*JW^jQr%hq`!K_p zysNi^!CsMgmjZMJ?}%jVT6;b`?-%H97t;|^>9O^y;E-Zn#2y9u_%&#fmT@onLO`lx z`217?i3ugM8l}z+lO0*%MUoj*cE^zx!X9vLS0`Tw-9gBP_OA(jCqle&<0fplKc|#_ z61I4Od_$~D=eODs3D*_vGyz;|pzrB}mGCbiWmj}y2Uvw20si#uwn0}1kIC!nJQrjU zo3rsmp^^-si=gu7q4O}<;Zq#9J^1YDV!Sqz5_v4oRB-z6cRlOh9cB%YZ6^(v6xu*X zJ{4WgMInwn>tyEqRUmKJV`O>jS&6*=NG-QoGQH3Sn%QeBZE-5#Fs`nb+Sal4C#Tfr z^}#9&5)0q;aXdTNk;8{Y%3GSYV22es@I);rEque`2b4Ga{4gI;z5t2JZ6BU+_eW*`*<=GDWD3kg)#m;{ecFajnfP2cg z){)`Q#)ng!#z>UfOASG<<6M;ZfOFl>Rqi&<6dvp~QHbJ#sG<+6k=tukr}+%6qATu) zkwR9R%4g+d*q{*a;VSR7z)X!$5sk)Gy@3ZGM*rX{8tx-_6%U1>N#~fjF$~u4m^*C| z?~N1K#_835=jlF(@Y?959P<}|9D(L})510wyAhL|M(H63T5H7zl=h7ky97tkt#Tko z?GAH?BS#oz=%9de=vpZTySNoq2;HNC{*=#x?iKa;Ncb8a9)T<|)5ybMzaz&pU1PUS zwD>W@KFP;coU`|H@mmd4A|ygqDN5iD+D{~UUD=Vs*!!g2=u(KOYwzVXpNA1cF99(M zwZAFeNX5vCMmmfm+3cL|4Qt{i9y82$QjZ1ctKqb-YAV)tN3SWDOG{R*_6VEU>zWAC zr5$WA)+N&d$rZ_-V*F##v37pauE#Pe$9WS0p+4UyWNfRoPfK3-3&)!S)_-FSH>}BDj~w7Wz+(BL;0Jn$=(Z0E;RyT3QOx*^VNQIV9FT|sU-oWYKJ z%Gh5+yxmF;8+r8L&C0r+1j59l6#ij@LSl>6 z-?6gJ$F`i_GF+K5cuVPutzw-IZma$G4!MjHRkb}>Sw~3`c6$QQmX^0tMUK&m zsWO|D;lt#=#98gn8%j`hAa-%T{Zgdj+@#T=tVr+ytFN|LJBFHRvro`q8BZ02W>Ih} z(eO90m%GgK>x%DTPnXG(mm$S)jsalcrPFKAJ^iu)!1p-L&X}~`9?jZx;KFLtyyW5z z)wwVd9}Vcd&Tu?I#lKd(q(=+wk=Li#2#cKUrT?RZFF_@CeWpLT;YQc*qUb?Z1~uqJ z+6K;73=5~}ourE9>ZKc{I!Yb0GoHI2KWZ3>6_(gD5wb`~`Hk#`nrCzY>P(R9tUB4k ze8+5q$1lTfrVgWubw$HRUFSvkvoss{kMytL(92||hPgkB`58`GCd)Ka`tKLw?ek<~ zqDj_S(~~2vS?vv}`GPqersb;N=>;!*9Lzkqj)44$kNYRYUhm{o&as7;?HE)+$$}`3 z5H@;Z7CIR^%1_V_pG<%tj(pul;Ulg6T{H401|Xuv^lEXf z@z*)Yj~B;P8n&d=IUPw7t95 zkGbv}KDp5c7?lyK$kD~xP3=>0@*M#jQN`~))_LNoxefbq0NNF5^w$sn9CSUtYS zZEf}sX?zyC8l#88y2y(9e?j_JA2^9^9fp!2x`nGFDl=7maG5e4PH$0O?1St!P!aw! zz^cB%JjL>v0_hFiGb}a^oG$|8Ba{X)o3T#V(#tB<8|@|n*GqcL^cwW!M&jZ&#rW=f0B>1r(jeWfUj>oj?Acnq*`cXdUN{(4G@KHypsgT>rz}FU*nYsX zxY{+z7H@Ed){W5G7bgvY5LNWh1VB#dSlq#?-)u;6A?C|U(HG`gM|wnrTx6x7ve(m0P0BfvRe*434S-|?c z>`D2u7N-O%^_$@CXI|`_)I#=eT#8gi0(>SF4mTWYuu8S;&>9fdQonTPa;woz8{rNz zr~Lh5qcBdaR@hZP`hoB-GFrLw5-qjrdryx>_Z?aNY(eJbT@;hb8@BtJS({Ai$GD6( zw#zTy3~#A}c+q<5`XJscKFaxVM0684AjA14>X(1U-_F=69JrkAr>);L#zU5PQ=^K4@b<2E$q z4%ct|0kMTn=S<4Oy#Jp6*|+D5KFZZoTn2V0+A_UCU!zC)*Id-!nY^o@1aWnjMa-?4 z$do#htl}v0M`^JzvHEMg+)5eBdVD^28l-b3W14Bi2Ij~x6w_mvBrl44eh=EnOY$|> zq3@bZDKT@71t2V0b?bF4F1=vD)7Yve;#C-Rvr+2_g>+60vb#A3Rf$sW>k}*94qmk} z#qOtRPC)1;>oH-wiCV61Y38m%-q*qW-fhr9)NFQU0S$dMjCvs*etlX_rV;uF=a^=#yVXc)SxT8vC7 z2;A`UbKb=mO6x1+Gz5^~?Ioq!*;s^WLA*Y`UZ}%C4^mFwa+xY2O&wj}A?n?vrz8xG z57|Sz=E(o63|;p+4>XXpMsauLe#GDHyUwj!lodPp5OW3Q+n4aez}Sg72p?Z~k1LVs zTpPH4P3`pZ(yCb*GkAwm#2?E^ny74`I4mFO-sXoqG00P&#n?@{QE96^$wa2}HM zRb4w`5wyjG!EelcqYc~O0s1znH^Nr$Q|bXFe0f=*;-~5%QP#AbCwtN$WRx{{ja#WHB2EZ|V2Ky^3=%@N#;mLK}#DSiSXU^AMzl zw}VHL!_MrobJs&2u9lC2PUGEMJ(oKA6vkrGNCFeq&al7mpD0|k^Cudgo~Ep9*#E5E z<>vKD7l8r#0lb*ytjMRac>6L2LeFqc=dDLvC;iC#M+2E<0pnlH4Pg~RP@Qt>_ zMvInSFKmS~urtxNL6%r)SV^_LS6J(Fq}Z=9h|xslNGSU?zZ*N^B=)# z@4;%rTW7a^R08LawR?vPI$nHPL}E&MJ+q@ZQnvbz#mDPVw9!qct z?QpJxK3BJf!GbB;;7`Bb2+}+MP(&;ljF_ha5hD0=N|RtdvB;lAF}W&HKhO*c{=zfq z?lX_iIP80aZVbwE7fHVxensLc>d(dyg;_49xK>li8LbRMZh0z-*h`t1_{k>`_s#tX8Y;(bN=)EtTkk*O<3lFDQ#0??-8l#4AH-o_K>|rI@`nTPxDXM!-~# z!`#Vcf;Xkjpkak&K|mwqV?|UMjbqL!gPn02*DV_vq{ti@{9kt~k>i+QqF zr@Jp$^0Z0pnPfTcGR!Tn+DlZw{(}fyIp=lh4!x_}TA}O|Z=VaClGoO|lHqZ=;NyED z#O%w|uoNl1am_fmXJ$+6#12;vU&6+hk7CC+O-o}W0m4++)eJ!jx~q^rl*8F~i=K|v zs7jZI3%Qy)LzL8A?Mq)Psj&{}n8o^WXIbkdqe6HD31W9VXiYAyg5y z+s8`(%O+!y*CuRarUSEfXLc{aowxxzhyK z*wg(X*~s=;%Iz;02*cL+cc_>8ulbme{LFjv^=9Y<==bqt1c%yVWMZ*s{BPSNo<4)+O3; z;n%K*e`)C2a-VT;Fv~<0l@%Hi0-Q8wkxhegk24JTIEQRTyBh^ZCWdMC!Uj0zJ4l8c zCwD+bMnGMV_Y*JEC5`#V*Gg}6mY0|0SkBpu%~vzuu| z72|c#x0RXmOj7mJi!b#?ibkEoe1{>3pssPqXkQxW0

5c4?G?T{#R8!87la5&=rpxF(;lW!+RfS?z6N||9&W9O0uie!i$K#H zGY&`oHoLc%*)n&cY$x9;)%ZLFPvU7e|v{?Xe{PUBCHV2cxUP zrPZXBuYvg6>hBetde&d$uwom-ioS?>%UCcxFYLrZ-EB+6(lYps8j~D%48HT+XL#iG zc7C2a6dhgHR$H4Q*PaIp91dF(D{7Qgdb2G%tjW=!vlQK;D*esC&q5@{kM^Tuop+|T z!)sAzS(o<#1@4vV_H)d}`O=3_*b7sobXqf*R2$@O>cr{~=;hR8v>M$*tsQlJu|Z)+ z1BU3^oNlzJM|{uLZ=mj%h&hx+fQhy_3xfPfFbxNx7)n*AgYthO1I0j=QbEPwi*M|CecsUH= zx`(LGS{3K+<<;&PivcLEd8$gAsYDA`qK`Q0TW2YKvzb!5>9KqwDTGr zj)Zvlv3nTgNc=n=uvtk@7b!Vsa562(8*Ag6Lr*o4C+D}8m%{ERr0oO^XX#-}8ChB4U)YBBm~4wPP3(q>bD}CA_j`51m`KAMmNvV`Shb^tf*9!Ow}xFk{9;U2WnFdQS``4wSbQXO zk_tXCFmJx0oEeAb0&UJl;9@2Q+#gpP!iC!N4I!F(;xdHGRPvNz4@}CEINcXIBe_A- zRLEXeqeJUGOZ8aE&@yL9^(n<3dM09XB~JwQkQzS!+LgU{G7^U9oppe1vRF7o)O{sS zS;t!{?G#MMhApeSs!gejDfS--#+>AMTZhY$X0{}eAB&YOF2H9<1`SP_Lz;_g+V!b7taj#-(Dv?+7J`5&Svn( z{3Kw1s&|^q7L7<{C!Aukiw<_xNrU6KCl$1oPv9wni{c47!Z(K0~Mkc1OjohqK zei|`SU5W-7RM=&1@lZ!!Rm(-{Em_ zzx=zI`_5)W0Ntmp%{vs` zf!PCh50ivQ<8LX&8$+tOS_B;dxfg9=@Z>Hpex*qo=wR$Fp1PYzK-ckm=;W;((|9e2 zv{R%iV6m3|FVdwF2d0QxtiUU@qFl(nyyKDC#X<+Oq#wy6YCscj%Ut67f+q14@|KvG z5i;Cw;b6vje?}HLPP@r7mq;}g+r9@?*#!QoH>kU7=^vlq#1q2y@szxGmcC%uU`ze= zR(l2)Kf>yqlR#JdJ2xl8X}EcB^sS~bAM)pMcFL%zCr{t?bbCo&u|+YOY;Wf7jB*5&!e^ov-V z)_O$-e1p>A@aRtDd}^?J_Ex8aVlOzH^qrwD+3Lza8tUgb`i zvz$YW=t(RHZnN>y)cH2YF%AZB5qJ;a->~?Lo;jsLI_#?}IPuSk4E08i6UBp(8TR$L zbKLn3QZ55&6Vry9C3SNuHq-Z=1oJn+$GRqa9JbK@@zvXP2xp-2Pj@Wg=FfNSt7v1Q zWJm<}{O~1NwfX>=vQY)X>%l-AyE?)+R7f=Bhk78LSIVu^Q>BiXK~_8L+R!0?$W>l~ zBhyNi^@K~bI@ljSbJG=ecmjW>fPKSD5!RgH$X`>Bzi z4Sf?~fVT6b-CAw&x`p*vts13RzCKm{I4tjsMxWGgMBE39(d5^bSOr#?u;%7t934MaWqGXiIYp*o+pvl?*O}ru5j6n&dfEkMv25-u>OOo@{u|uTDOyW)^00+3 z4kowVh|FA~vTr8g8{b-@R;DJbV&zX$t%C+W0W^r1inp;nK5oyBZS$D%VbNhL9?-%yPZem*2QVC{a# z3n41BBK#b}8t#am9*lg72R`a3?l~ zo4;IJq2(^96;x-)<4rte@ZkES>W!ho;zR*qo6KC)vOzc0a>@tS07!+LQnAXjV zTKOH2I@c+~IU;^}HG%VRc)&l=9+AmdgfF9DmDZOiQ*bai#cn@S46r9Yfw_F6hm?A? zICtSG(_Eg&e!p~j=dS~!%B!k5JzquDKB^u~szCU{*)g2|B!5w2oV@ot=D_wROw%uj zHoROx1Ee;ipXO=hU?RndoZ*grD-Nzkfo$N6xatJau%Fx$$SjarXIyPtEK~5-yGL_e zdn88Lj%e=W>7p4Q2^$9ysN1^{-C~G=$%cy3I`0K3HRavW@NTF=h6=mK0oBDbt`7rd zv*??R?Hnz&LsUzBFX8nlAuN8(9f^skuHE>3lswRci@sGt(JR1~#^zyR<^9Cl^NZuv zOTThMnx*UR255A6@)Ba(3&zG1PNP`+gkIb>J3!-UZvBqf_MedNW))a_`grf`tUDr; z;fhc3hb7hlNHDQP;1eL#^){N>#%5bCVd`ljVKaA<&SvI*GS%G8Xcw(XMBqoB^0FKI znJFlUlmCG`X9jb=N%~ClOxB3{3WPZa`$(K9HLWp&UG8Kq z@(u1pIE)VO41@&gI9%eCTQdf|ODR&ahGXm81ClljFQHwnKKkjK)x zcLPDJ`ZeFQiJqCy-BlNs2cGwWR)1w|M*uvF!ws=icrv1u+!Cx zEYUG`jY2#0#L7C2dO9ogxOaR2dTLQa!MdX8@|@@*W|y=B2ZXOT%*FLcWRi3xKg?eZ z>kp`#r)obyn}4C+O!Pb`LB!-Ljq6S1lEP`$+lZrCo6IV%Jv@%`St^+|eGH84rqHVA zT(1Mm!`0A+UFEA;9}j@Mjh5Q{I-Y(b$)dh*K%kZRe+-RM_JJMp#N(TI_P@^s#L8t3krXGIP@z1&>P z7)>WU7f;tsWZ#(ni2p3OR6kX4c=$+WIN&?Y%G!2_-I1OQ`mRk(O@N#t-a4+Xk!}}l z2-UStxmTPne-iOLsCO%4Vl#=b?WrWwsM->(!0VkN?+k;}y25!Sp>f;{DDDo3lh8j{ zEavVH?OFc>n9QNIO2vhPi2W*s?b$V6n=i0K>5!P~w&)_DWw!lV@nW|-qn_WSMaT<% z3G86C$FM%(wslv zDcsgWB8)uk)g>t()53l={=(aj8Ht?5wJTA}wlK*aI4(ClMBTegD^lWqPnqQswFIS) zDFf`%gXdmXK?0h9?jD*a5xmKutCXE(7U@85Ep5%WJ1klEWsu|amo8)l%P;Zih^AX= z3yna`R_Q;z9E2$Q#_lSUIi%2YnqVd`Nh|KHE4Y^u1X-JpAqFxzbCb?8nyxA6uC?Ju&6x z)9)=Y%aMkvDna_FU?bjL>qbJs?sLZ${HJa_i# z%@buQ*nuq1Xyf!VB627G8o4UreD;zhGA?& zLsaou^V@smOkS?)W22dQSN%2Nmk`bn*}PYi!#Jx|KV6M*`+0Ld#8KI)JZxCdi{M zySXab2*33gTv?GCyS=&vAA;IKAM6uyTO={rDv6qsJ7O+LkwZ!d@s?6NWm#f}oZ(cTZJNAr;^=ZH5y%)9R=HE@%9lRXJf`2$}@rt*BU zI&6TE^27%F$btC=^`mbJIZru8D4MJ|iIM8yKGfri5dlEQao$>R*qyMT{Irh-!@=L* zbj;LO7CcE+6}Q=_3Z5>2y!R9Dd94^&L%X{DUDn8KR0@nnktl@xCcn+XKZ0B$DCUnZ z!Rc&eMj8UTwD&QFW7}5EkfLg{(Jk4AECzVtSW-y+Yv&I#{W3}fM>TX`^Lh`mV9piF zH?Q0=bLaQzM0nW2M?PG%q}qUlf-xug&K|@{7RYoOr(SQhaXgCuo0m`Fbzr(d4wR1t z@R$aZ6%#J#^o#m{#`ITkrzE4%V97shdPF6a^%798JmTFCENwGUz_`)=`AG3s~Q2o*_eEE2AX`g8-qc|2{n6oJXFx$?@gs;m!b z6v+fP4;b-k$yf#b2|cGjJ8OWcJZEv9off4o1ftR6X&n3e^o58P#vW1y z@NLpNJm^x<%t^8*uYx7Z^tjL;K@cH&duD+H19P?H$r$xz2M$$~#V#kpbh;@(D|e|L zNJp>mYt@UTOaK_3lOrWKqmP!qDLl_TKh_ubdh&ZzDvcjBLhpRSc&GkV3b@{DchVYB z(kk77j4wPC^ojB$zTMqLn2)v>)y*fr?sN5kOmeFcZtFRvC=Hv>y{5ilnGi}K6wT2I zrJ{bW7hNv>*C=txpVI>vGDG^s3RyEr7HTulz`UMeQ4&^1-cCA)%7fv1AbZWry{Mu&!ePj7-aD8D1u5Ok)WK8NC zbIPs7YS!HTAe!s>Yws1>;F3>rvfG2|!Z_UF)Cp+{s2Wt!?Fn~e6L`OYB1SdbKT1$? zI3Hd(F&zHG$~!urOTeN&<;f2SvT9k2zG(f{-w!@;p+Ilj{0)1pqL3vd4*V7$w0TGTj;F&wXcsk)XRl{=-i9-IHMU9HsS3i^Tt?= zG~VS~9OWj}s*y!qtiQ*;nJCD1Spb`3_$chT z)@*;iY_ggs@M5d!Ii>qWwBI{R=Z+8H|h#5hB5&8?hW{aZffIVX%I z3Rt>haZi9Qur#7s+$UCe7k_<)nRL$LI)n$=JRo=w$+u_Nl?g4WVV-oL!sqPD2MuiS z07&&TjUyPyM*d8crN{1ppidn9*wK#${2etjj$_vw(~jH6Bo{UZJWLv$TN!;s#du5Dc{xlIri;CQV`!zO z!N+x0R>AKkw;R#xmFU4uSLBw~K?V=NBZYFGO(335>5BF-H?jtg?W=3Y7_wp_Y^GAF z{rqW+nf5_{v+Nr&SV@{(r7Jev)&|+2VWAs1b}E*|J@J-*@}#}*lN4CB@`d$FQYTkI zIQeW;i~r78UjlFF8rU4c(aTMG_pmmB z=H#hu^0gu757BUMGHWd9Gbd4 zaqgi6sFtgq>{zL-egti5CrRZ=wQb}BEvqEAa5X;+FDY`yMun<7#I{TjUbH)RCrF+X z8IUv@ITerJ?lkJ32rD%PggJVIr>$vv@put$RpM!u6tW1``f+ubcuAbc22(EaDp`UA zn&Z~U^#0!aek&w>79v_6tqGf=RJBH@?ccoT%krjYB-Sf%?0ILLhJvH>-GgevrbeET z>WBo0$iBb#`KwOLyk-@PT_~kSosDw(Epa9~yRGaEIt1d}YWx6uA2FH#*emrTKQRjynTT@W0~@7&M(?I`jyK4glv<5-u8m*I+GjBRS!Nr`Pb?n<9$3snn!Z=_V^J`s*~hJsdmQ^w&*M0i?*WR zII;vMaQaN$HuwdndSkrC`lb2Z$Xd0^wP5}y6wDyOVCL&Gic>v70)9JnK&<1~4ycR{!4vociecfiJ`794 zvG(oH{=8mucArV-{Z1iEY7~_DOtX9%Vqo!ekV-?wV*J{|W9^K^-WFWC*Qq+#{RxWO zW{0B4Nf^0PnR?Sj8{m4F>c}5h-co(=iyMnC8D#DLz7{;}G4t}zT(10DnXYsXKO%w~ zWGhtLXoN&pfXI6aEMAjOpen#BY%=B*f)W@Ako700(9<-Ohy}^;!oPXee^jvK3+o;p z@4p{NWe+t4A`)}AGGg}M0v*h^gZ$TGrTlT(qBZ;bf%UIcK&uH7==!bM0Eo7teAyX8 z;8}#qU>-f-MEEBnmb+u`IhLHB4RX*^AX-*|mbW*YXct_8$V`4rO^WIbteDzY( z%*ywG%C7Tby2ybZxJ;?8U_rtqwjc8=T(rK7Mc4Np2$z~(j+0Jvobj8yE-ZkQM4Pg7 zNDSxh2H%rY#FWS*ic6E2HhQT#>{`|CaLo2|-&klq{Yd)~u1nVY<$(fB>0Ai{NdZ`$ z+2$V(n=a0L#DNPrEKYiM+j8jayjBl#WJf75Kl~(2T_)SHluBywd%%y9+Q>%;0fTc; zzCqadrgBdD*crYhr_`D(T2#Z-x9}rkCFcp63P-q_ZQYicaq71X?Q0aqwT0x~bR|>H zi45HYC3@M$23b-rJ=Ix6-S21fP8Ra6L~v6RKFW~t5hQ8(S;y9$A?R<#^Qw7E2y6Xh z1@COoG!=ibPSxlfceQng_o_CrT#-~Vw&ietG%IH_)E|4KIa?0GaIUx{c@FMXcYCJK zy7gui4IyPe;M4Z+p71>3dZv5*A&I!aM{EKg33Kg>*<8=$XvLlEsUQ)9tVqJf*%MkU z!4WW@T63EcjIiAmsEX?Q;g;6~gC6Covd=@SVBPe9-6MWPP<@ih$!gr#+|w)zhi9OV zT7ZcFY;<}M;n>o{-nnsLt6I6G#eMbL<|r*s$V14HS)J{~IsP>W+6NHFGp!si)-v;= zs1_QsuKDqB{NnlH-Z-=}!ZaJ~QbT-r>|xZIXOd+}#n_T+aYiO6vDZV=jMigT-0yao z<1h?%q>){0bCK$+|FFGt8jnSPdTnYm>Hu>ovyjo5z*Mv(t-97u{8g8z%qafRVHbv~ zXjA3@-{#a^&u+(br@(S~Rpo%dy^1g8?K6fpa%sr)nn4MFg6Yu1I`MU~BioTBc?qJMjA46xv^?8&Fh^LkHL*=&1+ZH!e5LUe&qf0B20F5-V zv$<){)mr^0>3}o8x%#Rr-EBL|4UA`JsTg;y$SlX%xHzm)azB+a0b4g2GoK@gzcmNr z_qVl9lUD5;68|@5NG0m7nae;|x6++1Y1`Gv^8*enmcL2XA1BWVw)?mouvr5@u5N~^ zQOo+z-&M!`3c@4CTK_J69}Av&A_Q$s-94CIPl|0rStynAVsW-(*#&Up}S{aQ6Q3@V^hmG2<`$Q>*;vnH;pJ1EC>ygmq|_(En&C z`R}K@F`8bcv-BgzRsa3c|33MD9xxOAubtYyr|tjKP9M2kQp=f9XiUED|Ho%k)BS6w z>kjkz|Fm;bs$M+)T=4L(f-xw}y6SHsxMwJHGA!%2;va64u!yf-mrsq9dQ2W(NaDo9 zNdA#4iASntVOKl;{w=kf^*n)vHQnf$-F%)z_{0xtNFo_?RR6s}c-9lp z%H3J>B!wjT64pb}{C@7YmCLiblx0$2{@T!og%ze_sQqC`bUk4qA?!k|FC;8;6!`p) zhd7m;Smp|sTx;5Y+>ig`d!e}~4dmbCHP?myaPKn-JIq`gRO~*eCxaidz*MafQNO6b zM9rsoIpHBbKkQt``|hgJJELiz{*?M?Y;|Xq=0M2;ZvzaeK29Cpwy^Y5!D6ElH{()B z#<2ALtE63PCsSg*r}CHmC92`W&4jaWQsI_iCFfNdf1f_BlxLW29}kEn+hV5n+3p9= z9#W0-HX^hiY9DTZFWOfBHWmM^Cbczy)52z&iXIW&D)$w_dx@P#!M!m#A zWID`8#)~7!cST_njlaihdKzbwso=L^n?hjaJu*z4%S``gP1^U*dwtEk zwd0X;dgr$KzNPU)IpKc>Sfg*}e~=$5cpp|p6#rS={sgQQOZ`VM$=i>ycb6n{HF_BS z*=IgomMc195DnkD)qC-GKOAUG`@A`_r!M^OZlCqa*^HLoJ8u8mq-~9hAR-1B25}Si zMD+1`TA{9Nr-kl0SCr23H8GDJ{<~wgj2$t=Nq;h0rRSDX6Y3v$rjG)^2eBbLO4`Mb z*Q%X%?T79Y$Q$);_%q^Kuu%nLt~9qJS_*#tfec_Q8Y}eubaP;z%oeG8ImTMG6Z+r3 zwEr6Wm4fvTQSZ0{cuO*;e=M{D$#u}cxQ~n9B)>f*yTkwexyq(H^*WuOw-)?SzO5*q zb^4ztFd2Td>}Y$uWULfOUpFCJ?N}X{zr|jM7+5{Q$OB!gXUU6&F^hJyzgz8 zuOh?8e%}cvdw0FeG;Y0mK_1yRT9e4gBCzX&|XLJiSyaAP?}oYAlbEcx%~l zHG21+MaKido#ucw>X4PUW8Poyx$;uFRiu}(GKz*O7gm6)8nZj5E3WjoOkYy9?T1JpcLTczP0U3JeXV}lX*Sr6FJzw|&YYq23_kCUGc^v2UJHnR{L2)<|tES4uX5W?lY(z%a;N5;J^PQm!GdqdVyN4ev*ww zed?_^%4Z*i4LZGY?W3~#YAW&I93MQjH?O8Z`?;O;*WkE_-|YsFH?$aCN3G)7cq_Yu z5HBv0EDv}4@x6gkFC)VKxaq3$$AElP1Fk2P5~-yGF!VK=$t6tUTPW3FKUT(9ttF z%-eH@DY9n$b_$6Sc43=}6~s8S3&_~Gf%5rJ70}gbgk0~b2KXQ^8agB;jw4mMU>dvD z=*y_(ko;;bKh5R6t+flxReZ;@{WQzPuJYl-`^leOeq+AN8UP?cSf99vb(qm{lonZA zv!Uyc#0gCC@@1yu~v#hCjHfx9y{K|L|S{8QkcqodM zXIqs2#7|hRVUmbdAE4P#A|`r;T9$R&`%9i^<^llEdMO+MG>e}BUz2%=>Oi?ymegRlR2cC-jqq$?;B&LNV~@KST- za-sbIjtZ2LuBA<37@qqg~7r*as! zL?eM1fgw`oFl~9wuKWfS8DBWq(@>NtnOIowsl0hHXh~2uH}RNw3)0%{`^k7C+;54N zWph6u7Y{H2ouEpO>P+HOJPb<}qd7JjV^u>>41^p^x{d4A>ZoR%)b(yR25rK0!2KrX zJ-LTcu6{wDt&rPFXwkM(nbIIEsxle|qODYMi2aYq z+R|W7oe&o8VY7@ox1peem)nBn(igH|%3E1FSV+-R0_DU>V`Y)+8LihXa-RxJ{8-I* zMRlfb8!Z8MKIIp6mj9M7+dbDbpy&dEdol&8hBXP2W@+yFj#-bV|8`0G(z#$i`vRqe zG#tvO$9+``l8a-BOD8{rgpY!VAjWf@uP3j;MD?2{ z0xCLlQ>595FWtpq3Q&uRhqpu6Xp-FXxwe6s*S7U@v#BVXQx~4ZguR@IobOE0l9DU&lfzjCCR7KJ1ygQ{*Y*0LJF=W6Nr;!e_Uh&6T z9pS?<7~y^YRJH-j*Sc}`6&PVXU@{W0T8QSEQQ0@UjTy5RN9o^FT>k-7E?Dbifl;v& z{=dtSgZ8IoKV_Kf8D_Y{a6e$xk}41=iIXMbI`$T6Fv~SvsQ`p1qLczSlgOc>?vq|@ zQLe!nlheC})f#Xys$ssBNW=6mt(B3Q2)i%kd5XOm8XXsJCz|QDy|abLMgd%?!+~!h zb~LDJ7$u}Nm2X@q6Zp1=?D~G?JG*)%tFf_3Az54v5QEhp?TUuZRN01Uu{0=DT3DOk zO2k|CEiqO7eX2eA6;!ZQX@zuDQnY*OUoxy)9pa{Nl67%#&# z`fzo9g22f zpOv3~8UgcB*MfX#vIc+erREiFW|p=1U^WHb_FCcC-v8gBKHjxPFB0u$8D+Ge#`3L$UsRH>Mho;vk7bgwk3iT3-$J#1fhBfLODnmhBW!|Hjg^&k#jl#1#6$E4M7$JJI5O* z6S1MG39%!b<1$<=q?$1oUvuTiP=0n+UYxkeXa?OuObNEQQ(>PQbDmjQuY(bdZPNkG zTNHXZHV2~;rz5n=OS%WehQ4h^$^rpsg zpK!_2^AqxR@1pc!k8@pO7_)1tH|XEX$)B=G3rDAp2Y9h63=<+d6=kxzmuuxmEOL{++aH zO%LaVtYRKFy=4iY@ zuXha9Y@ZldtjMYA|C~+RscRuxbbg}cA-|%>vf@rl4HHR-@(;L?;ARyb;%wO4s(QbZq`j3&$)fE!>3l~XH6gq0Qzk5#Oq zsj8q)=7w3h{hhrxv@Zj#>{L@0CtUse@eoS^bRwegLUlY!ldCt`Q9iHIcR+>`b_hX8 z`_bMPfd`EmQtscoF$8l=6N|8gsR?nKtUq%fy?Pkw)DE60ze2|G8i=*I;?6H`i{o*w z6#v%Q&?Weeor=FX(Q~uRz2MOe_)tDvFC?*Ty=%|aX(OpHV`o#7GfxRm#PK%;?L}?Rdi_mEV9l@R0fmcpdHhF00!U1dX6~&(LDvYu9Krgf$xRZ`kR{%H| zoY6zlK(ugc%Bxo-Sh9JMqn+kxa4n8PU3er(4%a%@?~5ZhH$p(vJ|TcF*RKCjbv`H< z?_qzI0oiT3hE|x4*thws_l?Diu&e9e06%9{SWd}RuTOIa;A#*oitc1;M9Bs01QdQo z+8QpN>=s+z68*q^D_4oGnRvCUAl;lfWf7ZR*_yHg*>-Gjzg{uF8*9Fwq$7x8m>lav z#>bNtr?*VE&r;22A6KxsrbA`nl){!W!C_pn zn(FENTw83TrOnw%KIlI@UBb`isE^3L?3WtOybyKm-^>FRoBMP0JT$lXod5wgJ`mo| z0)Gf$;YYL0pXXF-tQiE;!0%4qNx2GgI*?2wu6_cL^Tgv*o$?qxnwb}43%RSqG*=0YJy?3?CCiNZzvCYh%&3O%&jrcRB?2LrWl{HBX>yOoL z&z#-t|A$WWVE>eBI#-NdO`qdcmhP17ap7yXQbQFUn1;1oH(8R(jm!1V##u}b^@V(8 zw7;y%w>=Tt-RuRHJwHp(mtNnL6vS~i+$&(zSKy#j_$ zc-*Yyhe!Z48uB@oHMll~>D?QMYF9TED#Q7f_)RqU!1wa$*V(v(4U) z{;-{)^v@$H zwZrc^yT*|<)Ugf8A{V zU@=zKsG4R)xzWftQU$p*%K=2Y~)UB;Uh%(C4 z?KmGz`Sh<}tJs>mT?GTVs!E^`8?Vnq^bq?vJbiLmRzp_Vqg6W@Ji{dIy&ndJxSk}_ zx#gt=2jMIV-cN+0FFs=$AwLu<=g&4)c&I3TsLrSL->$+TATcc8_iMGmm*@_0A9t@Q z5q*V1J->7^un*bZ-XvJD&LzSTGnF-yXsV!{c^jS8sZI(fbQxzgipV}z06Kk{`^`em z-5|O#DYm<+H5r-B@(oDk;BW`C*S9#-nBqMn`XxS;9=%XI?}(znqKC01Wgyij`=Y;r7bymhyJry&LOx;l}w(Fv~=WW7P z9M7bWu19y_N3{P0y@hX}N6r9$^nOpX8@MDfaxCCp!#~_rr>)v%CH$f#n<}N_c565Q zUiW>4jG+9!b)TV1bK#hJbX^`CPH@e=cw2AJblK+XTzh9=^I)^c+||cGElSW7MATJ4 ziwM@^BelEhbBxyUU8)I~tPV=CaQTj0y$oWc_G6af8rJCe_XhKJibl^EhdRr1FF{JO zHe4zA$RNTzpl;%6gdW(fz;s&vbY<_z@TtC_?`T(t`Bt&o?K?k7%oZm8BY{m6;=!zO zGV9+bRlUpEnOl^|Xb=T{SR1$CaZPhK!jOr)mqaB=BZjovUA$1fg3tj90ItLg=?J}s z0`HxW6Y>6nwn#Z$gn1GX$6I$-zUix3QNRg=Y;_V{L&hRvSjnnAreHXEZs`rpa z*rO6&IKD{yQaFgs7uikO&WDl#AK0XKFcxBKqLqnv7?f^N>OYsX))`6?X@K7eRXE44 zebrE5_pwV3&jI=Li!bw|6vBWPbQe|O1PvW`ho0jaWw(#IzSzUQ(Sz>6+%_IH9HqDM zsHGkfUw0rw-)JxTANj6`L-v%6IO<(dowoO5!NX7EwHky+5A~2jnFoX@Po*X7bX2^O ztfir`mphD9MZ}~LjS>RH%oe_pn}ADT>nG;+FTQx;FLL!RjC$ z84HV+#qw`Lj!`$n*Q-w;TP@v5Iah*7;n7BE8wd!z%p>wEF%GN9+}r6h_CWnZzN{lN zC;gA!Pg3b^UpHMLAA6_>aC*ILiMp zlJoTMxcRB-_tz-}Wil|-9J;MBBZxpmIMmOz8oSA7xH@$I#yUbaQ0n=uvB>pqK{y7X zh_S5Nqtc{xg1z92`h~>e)u`uk!udV2Uz+eh-Y+@PQbZ`f!`|{IQKf6vzWIEZM6vuP zRI7EJd^U3yIrXUbCyDodsu^g3?99)$dmJ|qkZhVV{VF=KBQ+hwGeiWdH}DLPN{Aj!dkFrV~N3InB@2vTskMb%C3sCPSy+*RT-4@pvl6AVG$zU% zKQ4dfjVLX=1%EHJvO4)lSZC!?02cCcyZ;o;d|Sg<2?*(7Ilu$DFku~ei{Ou`GP z!!zhKG`yLgC440Hhd)Zy%*LE2d-exkzSuuoPLfd>WF(Yeig-l5kQrM0AKDplvKp{6 zy=ss?x_=5!xQ6#jflzs{KsLEN7;bSY3P>nKNfB2*hZOx=QZTfQ(=t)Q!7;z~JFMI` z30}~rX~yJuFa#~yY-m3(Uwa(bBgkY?=Qp5~bRw=Lzcs%>Y26amPXxEjq}+z}mR&?p zOsQejSeyAV6jW*5cRv)GbAML+e758Mypl+`c_Q}xad=1ijt{p)EuObzar5~OcteKB z=i|gT3A=*mxPJ4}$`+<}3R!zTwhEJ)8JMRRbZKnV#s}mxE<^?2NzJL4y#`6zOW9B$ zaYV~umQ$fWc?AI=S5ULeR`lQLN2da6Mt7R3&R8L*nIQ^U^E>i2N>qg~GF}P(B;=(- zMJaywvE|-Bct4voh8^C*+G^<5*vHb?$b@r;dJuWeBb1->S1)Ndkhr0&1N@xZO;*n- z?!VvsE|(fMNvgTe;|NGH5RZwBQ3e@S1$T6pjr~q1gRCj@x)oP2M}G~|0%cula+eg? z)VR*4BB=l~6763ypT|+!;3an`zpT^yEld@LS^RhFWdEE)s27dRm>UC|y9aoFbXF(I zKKb*KD>+l>*<)selIt&RjUW2^y0aXSiZ5qdd1O6xya(yEt>5R|9ayDc$Bkz0;x@LN zB2l3hI?wqfOiD}Ca}A#Rk1EIvYu;v8nA>Tobr&}<-RpX$Yl-K0?9UlU?&>8vq3Ffm zlxfAI0kN>((Da;pA2SkSAbC7UfDV=JLe$W*bof4#0)v9q^g-FtXEN#bin7J6M%q$* zJyJinM9T~EXmRy+=i514K$JuB#ztbid=v~kg_w7vpD z!ke+DEzf=MwQI^@-EIOiq?C`i^%{N-6$DT@vIyya**Dy_%IRwy*?#E}@-V@Wb>(4+@J#t3;}CCw9We2_{d&5RN9ecwY!TLCA9zJR zN8ArwK^t&;ycHMv=%+yI?H9sO>-cq{14#y$!T+yD4o60|N+rrqMYxFls=iyN>=0>` z-TghO!dX3&#qdvTk}Nv$pNn@dr=c2PvwGf!O7uo(KfF`jAmYsC!mO``?*H zCw0DL77Z;7+jl5)l{fZ>o;ew}hxLMYrcu6-)<@C-GOQ>U@q2vT2krbgP}oQEw3b~Gu(t0Z5kuM!xypYce}cz-x80@1eHgd8?bA)8?1S7 zPJ!7erblH8trv0XIK0L}si`i(pz*HC{v+zRjVrm;TK~L6H4}ZOE8z7}U#=;LEkDwDYyS z%Al;hXx0hnC+x*(=sEV2h%P_u!!8 zI}UpIeRiBxl%fLbExt0Wtp9%g3T3*86<|no>Mrh#fobhyeUYmxd``-@>NG#^a5TY5 zOOa;TYhVF+3E>`!2B|OyuVAc>wScbRl?@EX^VQ*X45=%soXyii36~@sUbhGw_qZ1m z6=Q@wB#jX`i0ut(N#lVTV})>A*$j2l(go2=6bQXLc+BQ3v<-dmX=vIBMossx*FEMs2FYxbEyx~7oaYLaC$g@wDA1Wf(`#@KA z7y|~&WZE6lbeyqNY@|I@Q+W)|{Ot;}cd1sp2+P*)^+#w;ZkvJoF$|7LiT)=0B!nJD z_}!vcoXqQDw}c492%hb>?YT&We1+omJz)q~fITk2hse{u z#?yJeD{TzPVU*`fP`0o)tp?DlW1y{X7Z}nGaMd7zH>GE~+JtqFuTRmiL`FlExXNp& zS|?>2-a4rVP-hP8#U^XTS@1D65HFw|i^=7U3AuatCMRH_%fu(Tylrt)8N)|gjZ}r0 zI@{)?_3thj4j&%}swxJm-7H%qq3sCH#u3rA=DeI6(xk}P00zA!iS(4=48rz6kIYRs z?oRUy|8v=refER(8!d3QkphH%j~X<2SU<4ICCk~~h~G$Ow9ZRAdiku+fzvfQ=ZUh4 z-JD#M;nH*Z$Z=j;ug+nJD`NjUe(^4@5QGXr_Q`}L@M%rhIo5}+&g-xy2Uhpe96L;< zQ?k!)Yc1-E_glfD5A@Mru<#<4J8R5X-r1)KmL|DLVy=32+@QWf#n&J4EDyRzX&1ik zu7(*6PKwciVv}BTl{`I{PSifmNLaK!d~&^;xX*zWVKZuAtDaF|Gz3lB^)h^t^VO;18T{Av z6N(wH`N?cer_K;ycKB5izEM*kd@Sn%e@^(PR{epKe_~f=ySI zF4Ku0`opfyusN^ZK?HKjYrUyWCIW_Me|Nz8f!RH3P&=QxdI>qgtHAc2mEDm#rv%ka zu5+EgC5Opj^Lu&QQOeK}VE zfl&DuIn`)IeffA^xx5wQAy>9c&V>K|n%xi5jfaGyIkpeQA!+7r8j-}P>dhd`0QP6E zIL8pbkDCPjmudKfGbT1vhpOyhRR*!P7{wwk93#IT8W7Bx8Z~zEo<&M z5(VxMcOZue=ud(sMG;?kZSnv!;)%dRq`b9$b<|C zq1B7Uyi|Qb5_rF9J{z+rg?Ts8Dnnn|Bqd|`I7y*>%BAeVV(1kw6LdjmlB$`s=azZ0 zlJ41Jy*zo)Bm|##ni!xuW3-vfYC`|{nu!=ml!rMfSb`n~!_sDZD~RGW(e1uTPS2&K z((>)gsYAG+V6R`pVzby5F(uI|`*e;+u@B0w@PXB6sns%L=2H7*wNX6Ag-sEy?W}9k zlxpubQZmt-`!v^~$p+GqLA2dWB4sS9ccSgc*!(mCRPHRMp;(Qk`RRxwq~lGu1~MVB zazq?qKol8&ceDITZxX-q1FjJk(aJ~sX9qJ(R=7;ASTs9??xILH}OPxj2X+Ady;{+h7949-R2gBT^;W-c2`Br#DheN{vWa z*kw932Ig3nx|yxc(n$6EL-|OVQPW2|?n&NGqc_n~r&`y~H^8^!SX9qb$m!N4z#x}# z=CosUX*=6iI05j}r;7fk4t9Bksq68ko~BtvAHko?YLxwx@^(WxA53P!*@uYO={`aK zJ%!sDw;KK?*5jyAxL8)3j6K z(&zIW@?1BD3_3@>ijsLn>pfvC&6czR`EPenJ;_I=~&dS1vq zR0zTe%X&0K;oIqOPRCz|@co&=!@uSxluj>!X`9sNar!FKUEloI3YNbDcVpZ43inda$I8HxjgCw|-F=i-Eg_cFMoMBJr|Syy3t77N%1YGSgo4UY0mnnVBS-!8DMz?_3S~PA9v5N6u%DEpj1o5^Amiy)?JvSkq}IAUZUojyVjI zwRy_8^LhRA@?5iQJ1l(}(&gX_1`L*}URL}8BBL`*p22wKDGVv3*3)-n6+yiIOuQbs zfyuYGjv2v}T#(T|^|Pn`&y$o%dpj@_>X_cfmVJqiw*Y*q58=&VMFz#@EB-0sm0;ez z(SydY`WNT^AyZZ=6Qc7bfciW=fqantdzQRR){%tO^6fPl>GO2=)p|em)lNRW9Z|B^W@EcEA(G>cl8CT41xYge^(P~lpE%5SH8PXN;YDj= z7iIKXQ#U|m^6!vnNIH78qQ;h3%Pm6dON;o+)I2L;qcW4y3Wb!cxwod#)~moQj|kv8 zB~UIUV2z;tV6-mGYVbQOQu3hx;}3*^kFhwF863&wY&8-#j%e?H|?{e1ABE>fFl1Tk;~k7_I!f4;vd>ikB6k2S<# zQZlWFk@^D|pSLvD#uZseD(92bkah8+u_sxg!`|h(c(<-XF6mU%9p&$8g-qQqW4k*))$Gi|p@T<;JE$ zoDGZ{+dduG3>f$>aBN{9RZ5lMIGE8@v_x(!DaGL*MLjc_8&wOyITEMU*vX~;mxp0g z^QU1S&fZakLl4zHB$fi*Q1P07VvM4_ccs@(fU!VP^L>B*MA+1r$o88XJ8qiV znHssgK6^>|M>Ny8heyZ_@SM76Mkt(?3sC#~XjKL1Qs&cH8~eg1Mf%U}kOnf&Ku_<3 z_Zps&_CXBz%+f6UpP)X7dnqbPLFl){r<*S#FcoC9SSP?-IHip58O3^0vdw-e-WrG2 z`h7x{B^wPpEZ8 z#&g0)Wx%OgOY6NP|McN4rKDK$@Sd7OSY&!I;SFj!qmeJ)ff$&Xgu(A$Nj?aW>oa-! zAv3XyEs*Ldt2xaAYi~JIp4SFfro#BpD#RH{rj0Am>#}RbqZ_3gXBqz4c#SJlKex8N zrx$(Yk^J2Y^K8a5f_lDLelpWQe;xQT!WE=3;vA|qaa?Tj#)bZ zq)x*v{S?#U5G8}*@vB{FCVO@CugA+TnK1|T{s`|z{KF)YHBNko^%AQIT`Cq!Qcl78 zS==e1OCv@7fgEF81@@qC@b_-HKPAr2S?HZMZExD|WBlM2pWt|5=D#_Q0H;joF4hu4 zFTwIJ%{BNmVmk4Sz$31>T|Qaf=3n2%V%lE^^XJ&$%um#cnl;;3>Kr;=uQDp;r*96d z`uThoVLLed)4ja&oabg+8a)7TH9bRpLnB$^r2@$nzyj(izp1Dsh(Gr|w8N@x?93K| z957uB3y;U$r4O@~Ioh|}RU5etT5HcrRDW4EAsvCocgzJhGHq8;W0_ZI9MrDa)Km*O zt8`Nst(CqEafHQ-n#SW$4_R$8E>wVawa~WWD4pLF(ED3ckduPU1w|9h3XnVd2Y?~~ zne&Kc3IX5!sLBkqawa4Il#EM?Q%pC(H?Mw;BTsFMH-xNXYfJ#9H!_NSdV8c$;;F`BA$V?2IS=V1?%xC zE3cmTO$(Wt-stjoQj2Q=t0;XEb{h1nyaBY6#a_HBr^2FBbtrV@36ZyXZ@ldZ+N~LbxJShiBob4D|1gdF(>-1!6D(JSkc;!L=pXB}*V_z(S{>>1Y0^g`b6O=7cdevJu4n{MHY`Cv@qHf~Jj!Iw#4 z!?BE-{HNR?^LkOzr^KM=X8k2874eci)Ji1^M`ZvGg9?OHBuDlzz1wV-^X3CevVv)h zI*C)6?Q7Z<3m372VLII_aDKe2yyg zG033`2Kpizp2!G$DUTJn=xbERoqr_St@*Kn4n;WDd3S#uQLBs z{VXE6ORIoGg~JKzTA?%SiZsNF^{yx+k_!$;mvzQU0VCm0He9Jica=sa-PUBe5=mXC z!74oPp$ajOl2M`&kS8a0W2Bh2@G!U(N z)P?KeHS?}YMmAfPwKX+4mi=gL#g`App1t2gP1HR98Jp^gvbEJiOw$aNHHQUWeJO93 z4Q3cZ`-i>}zFktAAUraa#JdEd1dhjQv9EeWZ%csw!lboPZ@`?5_?-0K;#Qr;{N+fIS09j`EE zt|<%1oPL9REn59s-t8*yjCPQBk9%E|?%Ost)#%GjGX3JQ=rXYj-0tI-qudsl$1+7SPgol%xmfKt=pU2V~6wR#1czB%(>lj=S?bzs{O`+gao1R=d{a9MHWvxaaoI} z(UWsA3-Ilf@Xvj(@Kx!yS102{+8OP(t8HYan;qz@pRJRzr9Ml)m!)ngIjqa8E|uQM zSl$g?7JOaD#TQHg>C-K|swU%jmSLq`Yu<@-$G%^Bd!$ry*3- z9>bpx$lCpfy14C}p0+!!7MPYd)*M8{R9uS1TkLVWWM%m~tMTXYlV@C!0nJp`WhLRv zKn>$AHMH-_1wPBImGw2t{f^=*$Q{peUy!NJl**J>XQ%W>n=NypPLAHfX2^ZK zi7Fvl_kjiBUXK=3{ib_w-kS+a3g2Rd28Vi|Y%NQ>YK$?Rh1yGaRF~D_2`7bU+Qi2y zaWT&pb~@YpVL8|P4KX4YUDrB#!t{)_bbU%XgfiXn=i6wSroP(dzHQx#XM|}RM;C37 zqN@$r?DF!kk8sDnPF`F*$fU% z9ryn(YBuJ?ToCI7DTx&NXIQKD+ZY99tR*n^9-9RqwpJBexApiC4caP%)MhSa^B+Sh z9|D7B)9sD6)OWPw!85-jS9R^|zZ=DvoUWWLn@+Fcn_E%=WlevOf%}bIL3V6BWb`q* zCm-?IUJEJFjFo@3UpE!Wr0=RL9D6cBE+N9+pACBPGP~R!hm|pQj7vwr^FZ_&b6h;3 zKUsNjQkUpz&rgw(*-S?0idXmW?;!t~R1oXY27MR*(Uy*Y6(vzX)Dq@X84X6ZmzJwS zViXa_66LSY+M96E2r`$(t5+%KPbHw5om!btx8d*H7Jet*<3Cs5(zRsVSM&>xw8k67 zrgV8Q_1l=;Y)*a31@nhkICy7x95Ngw+bkzGd1t%R0)qaUAn=S<$89g>A8J&fCloHa@HG{gbSW|Z zl?*1ULBHBo-K9AfVs)J@1lIpTd0&v0DP+t7>iUq!e8JufHoS^U+x|>~E1>vXdBp;k zP{q)XG8KsQ>CPwMX!Z(xh98_(G=iPb(SL?z=k=jBOV@HXn$nsbP&9Em9k+O*V6{$| z#fg}w*L2JvqXbPNh$yKOhY4ZP$$Hc{bLMZNxvNQA*2sywom=vs)ll>s&4qMEn06Jh5;6{^ zDm0{PhIcGe{&62iRDB{_mubiMXhubJNFMEuy9mCsOpv`!MtjB6GU;4;wYdI!CPHLG zA+Sfvs3lOwzHg;g?=WCfC}z&5l%*&tI2&)-<7MPiE;W2?~^yI zd(u%N!tP8MyH%f^ykPT5y$0(rxzk6zW5x4I*VVFz`96UEt|QoW*78{VMBe{Wq@}A< z9)HPz#za*tqUD6aD8jGT+Iz+d)V3&BSm~p>)T>J4IWjL3a84z$SokBZE^+k1;4Sg& z+>n9J^#BHF6p51twk$N1);pp*(!x~H%e@yn=r|&#^=9JiQbp@!Pxc~X!~Nj*!l+99 zt0G|&V58ChUHx#ph!qE4vT2iXI3=h7`?>QE z;hcozG3be*;d(7Go=V9t2Kon=YOrx$aV#>+Rf>|NozpX34Ro2D2|rx^WTWG6l*?{& zRX0xzzhABm>11u1?ZG=TR-n{s!BW^s{Jd(eil&Yr`N&L)hj&5Fw^v#D`n6X7h~JOu zHe;q#_|=Il-ZSIAy4(v!Ib3Zt!uq%)p?XVFnHLX;*{01&b&=$64tyr!p;mm8;n`8k zJGSN#e#W@Ke&pvyS$V~l`o-RtjV*o=Mt}PW&qBB#)+o`vL~Qvnw8V^9j0AwvUj+|G z#DwcF3)H>dX4_>r!x$3*l>ZK7c~=OE^!f^^N4tQ$T;ScH9FgzAf%u!62dhPLzr$1I zJfn3&v)9KnubX`$kDRVS+%fuxu9=YM*)PzkBT49*FnX>wr;S$t|xZ9%isO7E$fymMymT98L;ZWR7$| zwmomrhIYDF7X^~dXf+dyyUmHWd9Ttbn2K%{f5ZvjvA><_q09U$!%+ zbEP%>6XbwveRQt$?-O!R4V2_;gbxa*84f?&5Z_MTfw69PrWlxX#Y^tHj`fY2hX~zQ zQ{6{RL!y`Z>H)le6DvIc^KIXR02g z)B|7@$d^J9v!H{Z0*)m53%srFB7tn-dm;4?1hLb& ze#KhopXk0!zVifl?;13{Cd+pb~2`SU?LOh1&sWM7}{{vaXlX#oQkg?OHckQ|Q;!(rC!xucWWwpv?DW`M z`B!%2ldgrn#TQJQ-^}nM$60@`s^qwUB^cKQFFWaPIgDnrc6F04m}5>f@W()t{`t5F ziPdV!C`rWHQ(2L+I&Sfpom8_@=>wDU#s*GQszzcS(uvbfc{TV4rzm(g1>(p1mIOn1 zM|=GGTtp1-O}lFGlke783Kxt-4)0!4D0skPe1AN1sY)DiQhZVx&0GKZ!&*9e7i3d= z1H8?Ye0uPAJ=aK$Oy5}{zmqYVs^P~5fg!7+b|5sSjch%s4AV(|p(f+__)~5uE0z67 zf~;x*D@yZ92=yuikV^2>&kZ@OPZIcc%7(c zk6*95ZAIcE+KQ?LxSV6kZbOJze`8-|X2+jW{i}B9srH&-h`yeCwof$DXGM zat}?m)D@MQZcyq(Nb4k%Fo(F!hD?*6OU`3#1Z@hlb$#^Lw5wB3FGaINJ|6 zX1g1Ch#jda_pV>VK%9{OiQH*TPLq&Xbxth()#|I`A$hd}y?&--(POArDb z)V25WXikHRuYzWiLYDt?w1hiP|A|gqhJt?3QO;y}9=K1c%3RiazVb=5S(G((53b#@ zs#^Tmy7YaV2TcE%-d}K?Le#-8E<4hQs^1rW!@R?5)IRcHiw!~yb!PytY$@>vB3MnC z@0nG8v7Qw)q-tlRGVYuTd64S$kPw}tchGG8+No@d%bJn$q@mozJE;zZ$62{`h7Y+) z4al2HNe7`xYmnNRs$JKT7=2yXdedCZzd}|E%g`RoG3Ib=nS2L<>fE6)#jB-aink=C zJxp3JdMz^f9l#=Zu3NyJUy@T4AjJ`-fXcfg)0%Xn<*!5oP~qv}zfW_9W7DNX3aW)3 z)EzsO4N3G&ShQx_3u5=!ajK(_!BPOCVkdb<&)e?PIe&iDKLHdZA{IeI3Fx7wUu3z3 zn68k=8=1dFb1?@H!JG<%JWS}KJWn~J)XUiLMbdcR$K!os+H7Q7GW0(bT*x=!sf6{h+VP?1x!%Oc%(*QQ_pT2d{Al80j zy-94m=+nc_{fk*)l<|ok)3X_Lwe(|V7Bs{__h?fJ`wN?b4Y58GtY?a&bZRM>s9ID`m6!z9+b z5%w#1=$1xbw_XNcyY%lc&u>-`d;NL};46o1E5>_u>Kyez?ExLZg!tGR*&{zNy}u4y z6swc^@Acdld2ALtf|Y(p?~Zc^wi&@PRvV)6?Q~~_!v9t=+mop%5rn*F0`aHJ4y{3H z)hHnGart$ED?I6F^-DWAWXFvHr_b`kwVe`@8mhm}MP=Lx?A6M!J&TInhi_}W;B4)= z1Us?Oe5WXhyU8Y!=rKDzp?6K-hcN`Vg76?k1NydZd6iheh0gOCOOswH)5z}1zj&u) zZ#|~S9n3;<^sbQMI7@;X(^$XEz>dyn0vTVc>PfnG{4jAjjK?5FpFmTUX}^ zsNk%qe|!kT7^aL_-8+1(O|PqOtyQxOM-l*_TVUKUm0p$k^ zw{7A13|)daSJOol>GV%f;XIlvpg6Dh%QV9ANIMc1Dc{c6yG>Kk(bm4tIT2_SPfqGX zgT`WdRnKX!EFdls^w$>4yZ7Q_eSJhA+MUouGm4V!Z2`9s$|Xc}d8;Z^yj3yw1>KNF zqrgnW=vDw|u$Sf{ta^U3XlOSj&FXNj>7ehQuY`qc^IBl5q87-7r^$)ie@#ShU2|RR zArH6^fJCfGBadbA1U5RA3{8C7U$i+X8sx7rLXKquHWrvn83{aYgL#u=;<_}dqgqAY zjM`Dtg4NTEBrXlPECQ+{Zb84w$OTV^Lv1aH?gsHO{JK^9;R!~aDfWZ?!e^nq>8*dY zg)o3!toJ;Zy`W8}m{HU)kyGo!9SPFx)%C^QGaU1T1hQxk7c#QoXMecshz(5@0Hfu* z#1d~r21$czp{87DpM&y0zJLCrpE8F0a|K)x6I~{P#+%F+7f+=aNF~DC#9SjHs7<^) zP25{`3WEh<67|$q4K@~VDIHZ&Sji%>OuK$rO!JT+V=nv$?L>PI7wK5fU`Xz(2FFJj6@w zHkZN|Q09DI|E>@;=95P#O6t{v>&QzrHsX^kFL25kVakcU%;BR^jTz{0Ytd;4@=O-f z&am_P(bvK0GS~19wPS(Eb5hF7uv#mnm z1^1H{X62HZ5g2=rdQfZ)k!otv=k6)WM#v?#L&(yex4Djk*7iRQ4k}vhz(0;DH8YVP zUTx+u?4x5anT5jH;+_+>bC1X^>}$l8(2A#78bwl*8w<~r277e*CN^_Zs$EbQicynB zU{t6)6iA3W$lR$wROinvEL>&|MQ__mF*WX*IdBIKy7Z##IWKx5=>@XyLa#)l0W-tp z8utPPRiNcLm5-@=s>CzksU}=G1#9&OCwlt|-vloj2@OZOgEO6mkt^>3 zGQ3yr-yI|>fqn8#c zJq6~QP|H&tj?!A|X2Kfi!xR0~5`x*2^qD8pzVznDK4gqD`D6#&8>?=0fvjFBPX7pM2895DxJ{;qu|eUhqMTb>Aq|v( zRpkd0^^Pa2sTS#Jy6E%Ln~u3SWF>gMTju*tt0m`Q40D@}%59vBNFL+^?Vo*mE6WpjiL}R!(1sN{Css;v z;)#TqscUN$bJUcR=?f7!i?CxPaQTLVP>V?ED)RE26=3=pHlEaEnsCdc6<=JzD*gEo zi2qy1tp;%=B7X~r^N+8*(`;TY17o9FKKw&jT)$A;y6@QWOKWPyJup>czt1Z|qb64Ldw$mOz KoT_yXPX8O0Lp10B From 00f3b919c4b674f92db21b2309cc3d6c1f5a7022 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 17 Aug 2025 02:41:01 -0700 Subject: [PATCH 099/102] Updated configuration file for new cleaning parameters and catchment climate functionality --- docs/Configuration/basins_metadata.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Configuration/basins_metadata.md b/docs/Configuration/basins_metadata.md index 4c1d24b6..48d3c0c4 100644 --- a/docs/Configuration/basins_metadata.md +++ b/docs/Configuration/basins_metadata.md @@ -6,9 +6,9 @@ To implement this feature RAT uses `basins_metadata` which is a csv file that is ![Screenshot of Basins_Metadata.csv](../images/configure/basins_metadata_sample.jpg) -It should definitely have the index `basin_name` for section `BASIN` as it needs to be different for all basins and same is true for `basin_id`. Rest all those parameters which do not vary between basins can be provided in the configuration file and those which will vary must be provided in the `basins_metadata` csv file. +It should definitely have the index `basin_name` for section `BASIN` as it needs to be different for all basins and same is true for `basin_id`. Rest all those parameters which do not vary between basins can be provided in the configuration file and those which will vary must be provided in the `basins_metadata` csv file. A special index `run`for section `BASIN` must be used in `basins_metadata` and it's value should be set to 1 to indicate which river basins should RAT run, otherwise 0. !!! tip_note "Tip" 1. To make use of `basins_metadata`, `multiple_basin_run` in `GLOBAL` section should be `true`. - 2. The values of `basin_name` from `basins_metadata` should be provided in the list `basins_to_process` for all the river basins for which you want to run RAT for. + 2. The values of `run` in `BASIN` must be provided as either 1 or 0 for each basin in the `basins_metadata` where 1 is for all the river basins for which you want to run RAT for. 3. A sample copy of `basins_metadata` is provided in 'params' folder inside `project_dir` with the name of 'basins_metadata_sample.csv'. \ No newline at end of file From 24e31791adf8ff9d873f29e87a106e2d7ff2ef44 Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 17 Aug 2025 02:42:06 -0700 Subject: [PATCH 100/102] Updated configuration parameters for catchment climate functionality and added new cleaning parameters --- docs/Configuration/rat_config.md | 116 ++++++++++++++++++++++++------- 1 file changed, 91 insertions(+), 25 deletions(-) diff --git a/docs/Configuration/rat_config.md b/docs/Configuration/rat_config.md index b2987c47..4e00277e 100644 --- a/docs/Configuration/rat_config.md +++ b/docs/Configuration/rat_config.md @@ -37,7 +37,22 @@ RAT config file has 12 major sections that defines several parameters which are -1 -2 ``` - + +*

*`cleaning`* :
+ Optional parameter + + Description : Boolean flag indicating whether to do cleaning during RAT run. If True, it cleans the data specified by the [`CLEAN UP`](#clean-up) section in the configuration file. + + Default : False + + Syntax : If you want to delete data produced by RAT and want to use `CLEAN UP` section, then + ``` + GLOBAL: + cleaning: True + ``` + !!! note + If you want to use [`CLEAN UP` section](#clean-up), `cleaning` must be True. + *
*`project_dir`* :
Required parameter @@ -143,19 +158,6 @@ RAT config file has 12 major sections that defines several parameters which are basins_metadata: /Cheetah/rat_project/params/basins_metadata_sample.csv ``` -*
*`basins_to_process`* :
- Required parameter - - Description : List of basins to run RAT for within the `basins_metadata`. The list values must match with the values of `basin_name` in `BASIN` section in `basins_metadata`. For more information, please look [multiple basin run](../basins_metadata). - - Default : It is blank by default and can be filled by the user. - - Syntax : If you want to run RAT for basins Sabine and Nueces, then - ``` - GLOBAL: - basins_to_process: ['Sabine','Nueces'] - ``` - ### Basin *
*`region_name`* :
@@ -712,23 +714,59 @@ This section of the configuration file describes the parameters defined by `rout 2. If `station_global_data` is `False`, AEC file names should be <'dam_name_column' value where spaces are replaced by '_'>. For example, the file name for a reservoir with 'dam_name' as 'Tehri Dam' will be 'Tehri_Dam.csv'. 3. Each AEC file should have two columns with headers as 'Elevation' and 'CumArea'. 'Elevation' should be in meters and 'CumArea' should be in square Kilometers. +*
*`catchment_vector_file`* :
+ Optional parameter + + Description : Absolute path of the catchment vector file where the geometry is represented by reservoirs' catchment polygons. It can be "global" (relative to basin) and will be automatically filtered for the basin. It can have unique id column and dam name column. + + Default : It is blank by default and can be filled by the user. + + Syntax : If `catchment_vector_file` has the path *'/Cheetah/rat_project/custom_files/catchments.geojson/'*, then + ``` + POST_PROCESSING: + catchment_vector_file : /Cheetah/rat_project/custom_files/catchments.geojson + ``` + +*
*`catchment_vector_file_columns_dict`* :
+ Optional parameter + + Description : Dictionary of column names for `catchment_vector_file`. The dictionary must have keys 'id_column' and 'dam_name_column' and their values should be the actual name of the corresponding columns respectively. + + Default : `{id_column : 'GRAND_ID', dam_name_column : 'DAM_NAME'}` + + Syntax : If `catchment_vector_file` has column names 'GRAND_ID' and 'DAM_NAME', then + ``` + POST_PROCESSIN: + catchment_vector_file_columns_dict: {id_column : 'GRAND_ID', dam_name_column : 'DAM_NAME'} + ``` + or + ``` + POST_PROCESSIN: + catchment_vector_file_columns_dict: + id_column: GRAND_ID + dam_name_column: DAM_NAME + ``` + ### Clean Up -*
*`clean_preprocessing`* :
- Required parameter +!!!note + To use this section, `cleaning` should be set to True in [Global Section](#global) of the configuration file. This section is Optional if `cleaning` is `False` or not provided in Global section. + +*
*`clean_processing`* :
+ Optional parameter - Description : `True` if you want to delete intermediate pre-processed data for a river basin except global raw data downloaded from servers after the RAT run. Otherwise, `False`. + Description : `True` if you want to delete intermediate pre-processed and post-processed data for a river basin except global raw data downloaded from servers after the RAT run. Otherwise, `False`. Default : `False` - Syntax : If you want to delete intermediate pre-processed data for a river basin, + Syntax : If you want to delete intermediate pre-processed and post-processed data for a river basin, ``` CLEAN_UP: - clean_preprocessing: True + clean_processing: True ``` *
*`clean_metsim`* :
- Required parameter + Optional parameter Description : `True` if you want to delete intermediate metsim inputs and outputs for a river basin after the RAT run. Otherwise, `False`. @@ -741,7 +779,7 @@ This section of the configuration file describes the parameters defined by `rout ``` *
*`clean_vic`* :
- Required parameter + Optional parameter Description : `True` if you want to delete intermediate vic inputs and outputs, and any vic initial soil state file that is older than 20 days, for a river basin after the RAT run. Otherwise, `False`. @@ -754,7 +792,7 @@ This section of the configuration file describes the parameters defined by `rout ``` *
*`clean_routing`* :
- Required parameter + Optional parameter Description : `True` if you want to delete intermediate routing inputs and outputs, and any routing initial state file that is older than 20 days, for a river basin after the RAT run. Otherwise, `False`. @@ -767,7 +805,7 @@ This section of the configuration file describes the parameters defined by `rout ``` *
*`clean_gee`* :
- Required parameter + Optional parameter Description : `True` if you want to delete gee produced small chunk files of surface area time series for a river basin after the RAT run. Otherwise, `False`. @@ -782,7 +820,7 @@ This section of the configuration file describes the parameters defined by `rout If `clean_gee` is `True`, it will not delete the final gee outputs that will be appended with new data in next RAT run. To delete that, use `clean_previous_outputs`. *
*`clean_altimetry`* :
- Required parameter + Optional parameter Description : `True` if you want to delete raw altimetry data that takes a lot of time to download for a river basin after the RAT run. Otherwise, `False`. @@ -796,8 +834,36 @@ This section of the configuration file describes the parameters defined by `rout !!!note If `clean_altimetry` is `True`, it will not delete the extracted altimetry data that will be appended with new data in next RAT run. To delete that, use `clean_previous_outputs`. +*
*`clean_basin_parameter_files`* :
+ Optional parameter + + Description : `True` if you want to delete parameter files related to the basin. Otherwise, `False`. + + Default : `False` + + Syntax : If you want to delete parameter files related to basin, + ``` + CLEAN_UP: + clean_basin_parameter_files: True + ``` + +*
*`clean_basin_meteorological_data`* :
+ Optional parameter + + Description : `True` if you want to delete combined meteorological data of the basin which is required to hot-start RAT for next RAT run. Otherwise, `False`. + + Default : `False` + + Syntax : If you want to delete combined meteorological data of the basin, + ``` + CLEAN_UP: + clean_basin_meteorological_data: True + ``` + !!! note + It should be noted that combined meteorological data is the data that helps in running VIC (hydrological model) without any spin-up for next run. Without this data, RAT needs to run the hydrological model for extra 2-3 years. You should use `clean_basin_meteorological_data` if you want to recreate combined meteorological data file for a river basin in the next RAT run. + *
*`clean_previous_outputs`* :
- Required parameter + Optional parameter Description : `True` if you want to delete previous outputs, gee extracted surface area time series and altimetry extracted height data produced by last RAT run. Otherwise, `False`. From 7d2feb8acacf54a9aa1307f5c70a86b16b2a1f1a Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 17 Aug 2025 03:10:43 -0700 Subject: [PATCH 101/102] Added plugins section in configuration docs --- docs/Configuration/rat_config.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/Configuration/rat_config.md b/docs/Configuration/rat_config.md index 4e00277e..774181c3 100644 --- a/docs/Configuration/rat_config.md +++ b/docs/Configuration/rat_config.md @@ -894,4 +894,8 @@ This section of the configuration file describes the parameters defined by `rout secrets: /Cheetah/rat_project/secrets/secrets.ini ``` !!! note - It will be left blank if '-s' argument is not provided in `rat init` command. \ No newline at end of file + It will be left blank if '-s' argument is not provided in `rat init` command. + +### Plugins + +This section of the configuration file is optional and can be used to run different Plugins as mentioned [here](../../Plugins/). It should be used to describe the parameters required by a plugin that you want to use. \ No newline at end of file From 6812a9ad06201ddc2cb46f7500b55ee9d212c5ff Mon Sep 17 00:00:00 2001 From: Sanchit Minocha Date: Sun, 17 Aug 2025 16:20:57 -0700 Subject: [PATCH 102/102] Updated test data & params link for gdrive and dropbox --- src/rat/cli/rat_init_config.py | 4 ++-- src/rat/cli/rat_test_config.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rat/cli/rat_init_config.py b/src/rat/cli/rat_init_config.py index 188b2f52..49132742 100644 --- a/src/rat/cli/rat_init_config.py +++ b/src/rat/cli/rat_init_config.py @@ -1,6 +1,6 @@ DOWNLOAD_LINKS_DROPBOX = { 'route_model': "https://www.dropbox.com/scl/fi/1kjivr13kyf6gn7wlhbzt/routing.zip?rlkey=zq8j501amqyirfprcgb9dquec&dl=1", - 'params': "https://www.dropbox.com/scl/fi/9qk9bwrbawryx79o7cclg/params.zip?rlkey=j5dqxipkvwuo3nso4dl1cxaux&dl=1", + 'params': "https://www.dropbox.com/scl/fi/zvluibsnhz36k4smzcavv/params.zip?rlkey=uqbxdi4ulr9dv4u4imktf605m&st=r0qrt2gd&dl=1", 'global_data': "https://www.dropbox.com/scl/fi/dhy3y3e9dw6tg89vt6x66/global_data.zip?rlkey=uvy6772cj5bpdfvtvqa4u8enj&st=4sti44e7&dl=1", 'global_vic_params': "https://www.dropbox.com/s/jsg2wu62qi2ltwz/global_vic_params.zip?dl=1", } @@ -8,7 +8,7 @@ ## For google drive link, https://drive.google.com/file/d//view?usp=sharing, use https://drive.google.com/uc?id= DOWNLOAD_LINKS_GOOGLE = { 'route_model': "https://drive.google.com/uc?id=1zr3VH0wy-XN-yF2_n0xic89_PiT_V_Mb", - 'params': "https://drive.google.com/uc?id=1LAGivWvgBdtJvDWzkGKorzfjPEKvO69k", + 'params': "https://drive.google.com/uc?id=1o5TH8GBmP4rjvFFROQmbid-ILMJw7xiS", 'global_data': "https://drive.google.com/uc?id=1Obm_cKPFoumJKhcNTvFxIZNkSU3OmgT_", 'global_vic_params': "https://drive.google.com/uc?id=16P95eu2yG0i77ac_NrmTSVutNtVXWNeA", } diff --git a/src/rat/cli/rat_test_config.py b/src/rat/cli/rat_test_config.py index b21450ae..79241ed4 100644 --- a/src/rat/cli/rat_test_config.py +++ b/src/rat/cli/rat_test_config.py @@ -1,7 +1,7 @@ from datetime import date DOWNLOAD_LINK_DROPBOX = { - 'test_data': "https://www.dropbox.com/scl/fi/f1pnyz9mo178kweh3agtf/test_data.zip?dl=1&rlkey=qxvsfrhc2li55dh4yj4a03bq2" + 'test_data': "https://www.dropbox.com/scl/fi/q88v9qpg20rhzhj8v4ktq/test_data.zip?rlkey=j6y1mlhc8vgzaicdofj4hn8g4&st=l7bcqmx1&dl=1" } ## For google drive link, https://drive.google.com/file/d//view?usp=sharing, use https://drive.google.com/uc?id=