From f0b2fdc2fa7a2ac93aa7dc736389835aab043125 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:29:47 -0500 Subject: [PATCH 01/76] Automatic ruff fixes Fixes ruff was able to make with `ruff check --fix` --- setup.py | 1 - test/__init__.py | 3 ++- test/test_HRRR_ztd.py | 3 --- test/test_checkArgs.py | 4 +--- test/test_downloadGNSS.py | 5 +---- test/test_gnss.py | 1 - test/test_llreader.py | 3 +-- test/test_processWM.py | 1 - test/test_raiderDelay.py | 2 -- test/test_scenario_2.py | 6 ------ test/test_scenario_4.py | 1 - test/test_temporal_interpolate.py | 1 - test/test_util.py | 1 - test/test_validators.py | 4 ++-- tools/RAiDER/aria/calcGUNW.py | 1 - tools/RAiDER/aria/prepFromGUNW.py | 2 +- tools/RAiDER/checkArgs.py | 1 - tools/RAiDER/cli/raider.py | 3 +-- tools/RAiDER/cli/validators.py | 6 +++--- tools/RAiDER/delayFcns.py | 2 +- tools/RAiDER/dem.py | 2 -- tools/RAiDER/gnss/processDelayFiles.py | 2 +- tools/RAiDER/interpolator.py | 2 +- tools/RAiDER/losreader.py | 12 ++++++------ tools/RAiDER/models/ecmwf.py | 3 +-- tools/RAiDER/models/era5.py | 1 - tools/RAiDER/models/hres.py | 1 - tools/RAiDER/models/hrrr.py | 5 ++--- tools/RAiDER/models/merra2.py | 1 - tools/RAiDER/models/weatherModel.py | 2 -- tools/RAiDER/processWM.py | 2 -- 31 files changed, 24 insertions(+), 60 deletions(-) diff --git a/setup.py b/setup.py index a8a76bc1e..9d18af2df 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ # RESERVED. United States Government Sponsorship acknowledged. # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -import re from pathlib import Path import numpy as np diff --git a/test/__init__.py b/test/__init__.py index a948eaff4..3de9227d8 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -37,7 +37,8 @@ def update_yaml(dct_cfg:dict, dst:str='temp.yaml'): Updates parameters in the default 'template.yaml' file. Each key:value pair will in 'dct_cfg' will overwrite that in the default """ - import RAiDER, yaml + import RAiDER + import yaml run_config_path = os.path.join( os.path.dirname(RAiDER.__file__), diff --git a/test/test_HRRR_ztd.py b/test/test_HRRR_ztd.py index babf21727..931cbae77 100644 --- a/test/test_HRRR_ztd.py +++ b/test/test_HRRR_ztd.py @@ -1,7 +1,4 @@ import os -import subprocess -import shutil -import glob from test import TEST_DIR diff --git a/test/test_checkArgs.py b/test/test_checkArgs.py index 9f210ee80..07240fc4a 100644 --- a/test/test_checkArgs.py +++ b/test/test_checkArgs.py @@ -3,8 +3,6 @@ import shutil import pytest -import multiprocessing as mp -import numpy as np import pandas as pd from test import TEST_DIR, pushd @@ -12,7 +10,7 @@ from RAiDER.cli import DEFAULT_DICT from RAiDER.checkArgs import checkArgs, makeDelayFileNames, get_raster_ext from RAiDER.llreader import BoundingBox, StationFile, RasterRDR -from RAiDER.losreader import Zenith, Conventional, Raytracing +from RAiDER.losreader import Zenith from RAiDER.models.gmao import GMAO diff --git a/test/test_downloadGNSS.py b/test/test_downloadGNSS.py index 284a26794..e798f1aed 100644 --- a/test/test_downloadGNSS.py +++ b/test/test_downloadGNSS.py @@ -1,13 +1,10 @@ -import os import pytest import requests from unittest import mock -from test import TEST_DIR, pushd -from RAiDER.dem import download_dem from RAiDER.gnss.downloadGNSSDelays import ( check_url,in_box,fix_lons,get_ID, - download_UNR,main, + download_UNR, ) # Test check_url with a valid and invalid URL diff --git a/test/test_gnss.py b/test/test_gnss.py index bd7598abe..337eb68b9 100644 --- a/test/test_gnss.py +++ b/test/test_gnss.py @@ -15,7 +15,6 @@ import pandas as pd from test import pushd, TEST_DIR -from unittest import mock SCENARIO2_DIR = os.path.join(TEST_DIR, "scenario_2") diff --git a/test/test_llreader.py b/test/test_llreader.py index c8b584014..b2699183a 100644 --- a/test/test_llreader.py +++ b/test/test_llreader.py @@ -11,8 +11,7 @@ from RAiDER.utilFcns import rio_open from RAiDER.llreader import ( - StationFile, RasterRDR, BoundingBox, GeocodedFile, Geocube, - bounds_from_latlon_rasters, bounds_from_csv + StationFile, RasterRDR, BoundingBox, GeocodedFile, bounds_from_latlon_rasters, bounds_from_csv ) SCENARIO0_DIR = os.path.join(TEST_DIR, "scenario_0") diff --git a/test/test_processWM.py b/test/test_processWM.py index e0dbbb4e1..7749fbbb7 100644 --- a/test/test_processWM.py +++ b/test/test_processWM.py @@ -1,7 +1,6 @@ import os import pytest -import numpy as np from test import TEST_DIR diff --git a/test/test_raiderDelay.py b/test/test_raiderDelay.py index 446ee0b91..f3a86947e 100644 --- a/test/test_raiderDelay.py +++ b/test/test_raiderDelay.py @@ -1,5 +1,3 @@ -from argparse import ArgumentParser, ArgumentTypeError -from datetime import datetime, time from RAiDER.cli.raider import drop_nans diff --git a/test/test_scenario_2.py b/test/test_scenario_2.py index 9d366c919..21517943f 100644 --- a/test/test_scenario_2.py +++ b/test/test_scenario_2.py @@ -1,11 +1,5 @@ -import os -import pytest -import subprocess -from test import TEST_DIR -import numpy as np -import xarray as xr #TODO: include GNSS station test # @pytest.mark.long diff --git a/test/test_scenario_4.py b/test/test_scenario_4.py index c5d1bd7f3..f1ed25efd 100644 --- a/test/test_scenario_4.py +++ b/test/test_scenario_4.py @@ -6,7 +6,6 @@ import numpy as np from pyproj import CRS -import RAiDER from RAiDER.delay import tropo_delay, _get_delays_on_cube from RAiDER.llreader import RasterRDR from RAiDER.losreader import Zenith diff --git a/test/test_temporal_interpolate.py b/test/test_temporal_interpolate.py index f78de9c90..50b88910a 100644 --- a/test/test_temporal_interpolate.py +++ b/test/test_temporal_interpolate.py @@ -1,7 +1,6 @@ import glob import shutil -import pandas as pd from test import * diff --git a/test/test_util.py b/test/test_util.py index db538aa86..ca3105878 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -1,5 +1,4 @@ import datetime -import h5py import os import pytest diff --git a/test/test_validators.py b/test/test_validators.py index 4e348695d..a46db9187 100644 --- a/test/test_validators.py +++ b/test/test_validators.py @@ -6,14 +6,14 @@ import numpy as np -from test import TEST_DIR, pushd +from test import TEST_DIR SCENARIO = os.path.join(TEST_DIR, "scenario_4") from RAiDER.cli import AttributeDict from RAiDER.cli.validators import ( - modelName2Module, getBufferedExtent, isOutside, isInside, + getBufferedExtent, isOutside, isInside, enforce_valid_dates as date_type, convert_time as time_type, enforce_bbox, parse_dates, enforce_wm, get_los ) diff --git a/tools/RAiDER/aria/calcGUNW.py b/tools/RAiDER/aria/calcGUNW.py index 12c18cf91..4c8033db0 100644 --- a/tools/RAiDER/aria/calcGUNW.py +++ b/tools/RAiDER/aria/calcGUNW.py @@ -6,7 +6,6 @@ import xarray as xr import numpy as np import RAiDER -from RAiDER.utilFcns import rio_open from RAiDER.logger import logger from datetime import datetime import h5py diff --git a/tools/RAiDER/aria/prepFromGUNW.py b/tools/RAiDER/aria/prepFromGUNW.py index 2d76bfaa7..6b6a826fb 100644 --- a/tools/RAiDER/aria/prepFromGUNW.py +++ b/tools/RAiDER/aria/prepFromGUNW.py @@ -384,7 +384,7 @@ def update_yaml(dct_cfg:dict, dst:str='GUNW.yaml'): with open(dst, 'w') as fh: yaml.safe_dump(params, fh, default_flow_style=False) - logger.info (f'Wrote new cfg file: %s', dst) + logger.info ('Wrote new cfg file: %s', dst) return dst diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index 69c170b61..b136748a0 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -14,7 +14,6 @@ from RAiDER.losreader import Zenith -from RAiDER.llreader import BoundingBox from RAiDER.logger import logger diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 07546cb41..fabc9d699 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -1,6 +1,5 @@ import argparse import datetime -import glob import os import json import shutil @@ -75,7 +74,7 @@ def read_run_config_file(fname): group_keys = ['date_group', 'time_group', 'aoi_group', 'height_group', 'los_group', 'runtime_group'] for key in group_keys: - if not key in params.keys(): + if key not in params.keys(): params[key] = {} # Parse the user-provided arguments diff --git a/tools/RAiDER/cli/validators.py b/tools/RAiDER/cli/validators.py index 78c470248..b25c0cf50 100755 --- a/tools/RAiDER/cli/validators.py +++ b/tools/RAiDER/cli/validators.py @@ -12,7 +12,7 @@ from textwrap import dedent from time import strptime -from RAiDER.llreader import BoundingBox, Geocube, RasterRDR, StationFile, GeocodedFile, Geocube +from RAiDER.llreader import BoundingBox, RasterRDR, StationFile, GeocodedFile, Geocube from RAiDER.losreader import Zenith, Conventional from RAiDER.utilFcns import rio_extents, rio_profile from RAiDER.logger import logger @@ -302,7 +302,7 @@ def convert_time(inp): raise ValueError( 'Unable to coerce {} to a time.'+ - 'Try T%H:%M:%S'.format(inp) + 'Try T%H:%M:%S'.format() ) @@ -343,7 +343,7 @@ def getBufferedExtent(lats, lons=None, buf=0.): except AttributeError: if (isinstance(lats, tuple) or isinstance(lats, list)) and len(lats) == 2: out = [min(lats) - buf, max(lats) + buf, min(lons) - buf, max(lons) + buf] - except Exception as e: + except Exception: raise RuntimeError('Not a valid lat/lon shape or variable') return np.array(out) diff --git a/tools/RAiDER/delayFcns.py b/tools/RAiDER/delayFcns.py index a71de3b21..22f8485db 100755 --- a/tools/RAiDER/delayFcns.py +++ b/tools/RAiDER/delayFcns.py @@ -43,7 +43,7 @@ def getInterpolators(wm_file, kind='pointwise', shared=False): hydro = np.array(hydro).transpose(1, 2, 0) if np.any(np.isnan(wet)) or np.any(np.isnan(hydro)): - logger.critical(f'Weather model contains NaNs!') + logger.critical('Weather model contains NaNs!') # If shared interpolators are requested # The arrays are not modified - so turning off lock for performance diff --git a/tools/RAiDER/dem.py b/tools/RAiDER/dem.py index 7d6099c46..ef4bdd403 100644 --- a/tools/RAiDER/dem.py +++ b/tools/RAiDER/dem.py @@ -5,11 +5,9 @@ # RESERVED. United States Government Sponsorship acknowledged. # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -from logging import warn import os import numpy as np -import pandas as pd import rasterio from dem_stitcher.stitcher import stitch_dem diff --git a/tools/RAiDER/gnss/processDelayFiles.py b/tools/RAiDER/gnss/processDelayFiles.py index bd80c829e..c4a5ec076 100644 --- a/tools/RAiDER/gnss/processDelayFiles.py +++ b/tools/RAiDER/gnss/processDelayFiles.py @@ -369,7 +369,7 @@ def main(raiderFile, ztdFile, col_name='ZTD', raider_delay='totalDelay', outName dfr = dfr.drop(columns=[col for col in dfr if col not in expected_data_columns]) dfz = pd.read_csv(ztdFile, parse_dates=['Date']) - if not 'Datetime' in dfz.keys(): + if 'Datetime' not in dfz.keys(): dfz.rename(columns={'Date': 'Datetime'}, inplace=True) # drop extra columns expected_data_columns = ['ID', 'Datetime', 'wet_delay', 'hydrostatic_delay', diff --git a/tools/RAiDER/interpolator.py b/tools/RAiDER/interpolator.py index 1b02f0030..eda923c16 100644 --- a/tools/RAiDER/interpolator.py +++ b/tools/RAiDER/interpolator.py @@ -8,7 +8,7 @@ import numpy as np import pandas as pd -from scipy.interpolate import interp1d, RegularGridInterpolator as rgi +from scipy.interpolate import interp1d from RAiDER.interpolate import interpolate diff --git a/tools/RAiDER/losreader.py b/tools/RAiDER/losreader.py index 3faaa7ef5..a101cab68 100644 --- a/tools/RAiDER/losreader.py +++ b/tools/RAiDER/losreader.py @@ -185,7 +185,7 @@ class Raytracing(LOS): def __init__(self, filename=None, los_convention='isce', time=None, look_dir = 'right', pad=600): '''read in and parse a statevector file''' if isce is None: - raise ImportError(f'isce3 is required for this class. Use conda to install isce3`') + raise ImportError('isce3 is required for this class. Use conda to install isce3`') super().__init__() self._ray_trace = True @@ -238,7 +238,7 @@ def getLookVectors(self, ht, llh, xyz, yy): Calculate look vectors for raytracing ''' if isce is None: - raise ImportError(f'isce3 is required for this method. Use conda to install isce3`') + raise ImportError('isce3 is required for this method. Use conda to install isce3`') # TODO - Modify when isce3 vectorization is available los = np.full(yy.shape + (3,), np.nan) @@ -263,7 +263,7 @@ def getLookVectors(self, ht, llh, xyz, yy): delta_range=10.0) sat_xyz, _ = self._orbit.interpolate(aztime) los[ii, jj, :] = (sat_xyz - inp_xyz) / slant_range - except Exception as e: + except Exception: los[ii, jj, :] = np.nan return los @@ -606,7 +606,7 @@ def state_to_los(svs, llh_targets): >>> LOS = losr.state_to_los(*svs, [lats, lons, heights], xyz) ''' if isce is None: - raise ImportError(f'isce3 is required for this function. Use conda to install isce3`') + raise ImportError('isce3 is required for this function. Use conda to install isce3`') # check the inputs if np.min(svs.shape) < 4: @@ -670,7 +670,7 @@ def get_radar_pos(llh, orb): sr: ndarray - Slant range in meters ''' if isce is None: - raise ImportError(f'isce3 is required for this function. Use conda to install isce3`') + raise ImportError('isce3 is required for this function. Use conda to install isce3`') num_iteration = 30 residual_threshold = 1.0e-7 @@ -774,7 +774,7 @@ def get_orbit(orbit_file: Union[list, str], ''' if isce is None: - raise ImportError(f'isce3 is required for this function. Use conda to install isce3`') + raise ImportError('isce3 is required for this function. Use conda to install isce3`') # First load the state vectors into an isce orbit svs = np.stack(get_sv(orbit_file, ref_time, pad), axis=-1) diff --git a/tools/RAiDER/models/ecmwf.py b/tools/RAiDER/models/ecmwf.py index 239dfc7fc..2cdf42437 100755 --- a/tools/RAiDER/models/ecmwf.py +++ b/tools/RAiDER/models/ecmwf.py @@ -1,4 +1,3 @@ -from abc import abstractmethod import datetime import numpy as np @@ -234,7 +233,7 @@ def _get_from_cds( try: c.retrieve('reanalysis-era5-complete', dataDict, outname) - except Exception as e: + except Exception: raise Exception diff --git a/tools/RAiDER/models/era5.py b/tools/RAiDER/models/era5.py index c06593029..f55d78ae7 100755 --- a/tools/RAiDER/models/era5.py +++ b/tools/RAiDER/models/era5.py @@ -4,7 +4,6 @@ from pyproj import CRS from RAiDER.models.ecmwf import ECMWF -from RAiDER.logger import logger class ERA5(ECMWF): diff --git a/tools/RAiDER/models/hres.py b/tools/RAiDER/models/hres.py index f883b38c6..2ef341073 100755 --- a/tools/RAiDER/models/hres.py +++ b/tools/RAiDER/models/hres.py @@ -8,7 +8,6 @@ from RAiDER.models.weatherModel import WeatherModel, TIME_RES from RAiDER.models.model_levels import ( LEVELS_91_HEIGHTS, - LEVELS_25_HEIGHTS, A_91_HRES, B_91_HRES, ) diff --git a/tools/RAiDER/models/hrrr.py b/tools/RAiDER/models/hrrr.py index 3d55e501b..661ae1a56 100644 --- a/tools/RAiDER/models/hrrr.py +++ b/tools/RAiDER/models/hrrr.py @@ -1,6 +1,5 @@ import datetime import os -import rioxarray import xarray import numpy as np @@ -11,9 +10,9 @@ from pyproj import CRS, Transformer from shapely.geometry import Polygon, box -from RAiDER.utilFcns import round_date, transform_coords, rio_profile, rio_stats +from RAiDER.utilFcns import round_date from RAiDER.models.weatherModel import WeatherModel, TIME_RES -from RAiDER.models.model_levels import LEVELS_50_HEIGHTS, LEVELS_137_HEIGHTS +from RAiDER.models.model_levels import LEVELS_50_HEIGHTS from RAiDER.logger import logger HRRR_CONUS_COVERAGE_POLYGON = Polygon(((-125, 21), (-133, 49), (-60, 49), (-72, 21))) diff --git a/tools/RAiDER/models/merra2.py b/tools/RAiDER/models/merra2.py index c0c111e3e..7e98a9de1 100755 --- a/tools/RAiDER/models/merra2.py +++ b/tools/RAiDER/models/merra2.py @@ -1,4 +1,3 @@ -import io import os import xarray diff --git a/tools/RAiDER/models/weatherModel.py b/tools/RAiDER/models/weatherModel.py index 7ad457668..ba21d545b 100755 --- a/tools/RAiDER/models/weatherModel.py +++ b/tools/RAiDER/models/weatherModel.py @@ -3,8 +3,6 @@ from abc import ABC, abstractmethod import numpy as np -import netCDF4 -import rioxarray import xarray from pyproj import CRS diff --git a/tools/RAiDER/processWM.py b/tools/RAiDER/processWM.py index e84cb55c7..4e677cbd2 100755 --- a/tools/RAiDER/processWM.py +++ b/tools/RAiDER/processWM.py @@ -10,10 +10,8 @@ import matplotlib.pyplot as plt import numpy as np -from typing import List from RAiDER.logger import logger -from RAiDER.utilFcns import getTimeFromFile from RAiDER.models.weatherModel import make_raw_weather_data_filename, checkContainment_raw from RAiDER.models.customExceptions import * From 6d00fbefb2c12d3948d692a8aedbb676bd90ecb2 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 11 Jul 2024 14:34:03 -0500 Subject: [PATCH 02/76] Eliminate star imports --- test/__init__.py | 3 --- test/test_intersect.py | 7 +++++-- test/test_losreader.py | 4 +++- test/test_slant.py | 8 +++++++- test/test_synthetic.py | 11 ++++++++++- test/test_temporal_interpolate.py | 9 ++++++++- test/test_validators.py | 4 +--- test/test_weather_model.py | 2 +- tools/RAiDER/cli/__init__.py | 1 - tools/RAiDER/cli/raider.py | 4 +++- tools/RAiDER/models/weatherModel.py | 4 +++- tools/RAiDER/processWM.py | 7 ++++--- 12 files changed, 45 insertions(+), 19 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index 3de9227d8..335d716bb 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,7 +1,4 @@ import os -import pytest -import subprocess -import shutil import string import random from contextlib import contextmanager diff --git a/test/test_intersect.py b/test/test_intersect.py index 25b0e4269..3dcccf5b1 100644 --- a/test/test_intersect.py +++ b/test/test_intersect.py @@ -1,10 +1,13 @@ +import pytest +import os import pandas as pd -# import rasterio +import subprocess +import numpy as np from scipy.interpolate import griddata import rasterio -from test import * +from test import TEST_DIR, WM_DIR, update_yaml @pytest.mark.parametrize('wm', 'ERA5'.split()) diff --git a/test/test_losreader.py b/test/test_losreader.py index 2a7125354..16fb0a0c3 100644 --- a/test/test_losreader.py +++ b/test/test_losreader.py @@ -1,3 +1,5 @@ +import pytest +import os import datetime import numpy as np import RAiDER @@ -12,7 +14,7 @@ Zenith, ) -from test import * +from test import ORB_DIR @pytest.fixture diff --git a/test/test_slant.py b/test/test_slant.py index cbe405b12..598d2f2e1 100644 --- a/test/test_slant.py +++ b/test/test_slant.py @@ -1,9 +1,15 @@ +import pytest import glob +import os +import subprocess +import shutil import numpy as np import xarray as xr -from test import * +from test import ( + TEST_DIR, WM_DIR, ORB_DIR, update_yaml, make_delay_name +) @pytest.mark.parametrize('weather_model_name', ['ERA5']) def test_slant_proj(weather_model_name): diff --git a/test/test_synthetic.py b/test/test_synthetic.py index 45acd7a85..0ac1582e1 100644 --- a/test/test_synthetic.py +++ b/test/test_synthetic.py @@ -1,14 +1,23 @@ +import pytest + import os.path as op from dataclasses import dataclass from datetime import datetime +import os +import numpy as np +import xarray as xr +import subprocess + from RAiDER.llreader import BoundingBox from RAiDER.models.weatherModel import make_weather_model_filename from RAiDER.losreader import Raytracing, build_ray from RAiDER.utilFcns import lla2ecef from RAiDER.cli.validators import modelName2Module -from test import * +from test import ( + TEST_DIR, ORB_DIR, WM_DIR, update_yaml +) def update_model(wm_file:str, wm_eq_type:str, wm_dir:str='weather_files_synth'): diff --git a/test/test_temporal_interpolate.py b/test/test_temporal_interpolate.py index 50b88910a..1bd3fdee3 100644 --- a/test/test_temporal_interpolate.py +++ b/test/test_temporal_interpolate.py @@ -1,8 +1,15 @@ +import pytest import glob import shutil +import os +import subprocess +import numpy as np +import xarray as xr -from test import * +from test import ( + WM, TEST_DIR, update_yaml +) from RAiDER.logger import logger diff --git a/test/test_validators.py b/test/test_validators.py index a46db9187..071830cec 100644 --- a/test/test_validators.py +++ b/test/test_validators.py @@ -7,17 +7,15 @@ import numpy as np from test import TEST_DIR -SCENARIO = os.path.join(TEST_DIR, "scenario_4") from RAiDER.cli import AttributeDict - - from RAiDER.cli.validators import ( getBufferedExtent, isOutside, isInside, enforce_valid_dates as date_type, convert_time as time_type, enforce_bbox, parse_dates, enforce_wm, get_los ) +SCENARIO = os.path.join(TEST_DIR, "scenario_4") @pytest.fixture def parser(): diff --git a/test/test_weather_model.py b/test/test_weather_model.py index 441c1aebc..6bf0b8b56 100644 --- a/test/test_weather_model.py +++ b/test/test_weather_model.py @@ -23,7 +23,7 @@ from RAiDER.models.gmao import GMAO from RAiDER.models.merra2 import MERRA2 from RAiDER.models.ncmr import NCMR -from RAiDER.models.customExceptions import * +from RAiDER.models.customExceptions import DatetimeOutsideRange _LON0 = 0 diff --git a/tools/RAiDER/cli/__init__.py b/tools/RAiDER/cli/__init__.py index 1f9447773..0368c77ee 100644 --- a/tools/RAiDER/cli/__init__.py +++ b/tools/RAiDER/cli/__init__.py @@ -1,4 +1,3 @@ -import os from RAiDER.constants import _ZREF, _CUBE_SPACING_IN_M class AttributeDict(dict): diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index fabc9d699..9f1646e85 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -20,7 +20,9 @@ from RAiDER.cli.parser import add_out, add_cpus, add_verbose from RAiDER.cli.validators import DateListAction, date_type from RAiDER.models.allowed import ALLOWED_MODELS -from RAiDER.models.customExceptions import * +from RAiDER.models.customExceptions import ( + NoWeatherModelData, DatetimeFailed, TryToKeepGoingError, WrongNumberOfFiles +) from RAiDER.utilFcns import get_dt from RAiDER.s1_azimuth_timing import get_s1_azimuth_time_grid, get_inverse_weights_for_dates, get_times_for_azimuth_interpolation diff --git a/tools/RAiDER/models/weatherModel.py b/tools/RAiDER/models/weatherModel.py index ba21d545b..4b08ee6d9 100755 --- a/tools/RAiDER/models/weatherModel.py +++ b/tools/RAiDER/models/weatherModel.py @@ -16,7 +16,9 @@ from RAiDER.interpolator import fillna3D from RAiDER.logger import logger from RAiDER.models import plotWeather as plots, weatherModel -from RAiDER.models.customExceptions import * +from RAiDER.models.customExceptions import ( + DatetimeOutsideRange +) from RAiDER.utilFcns import ( robmax, robmin, calcgeoh, transform_coords, clip_bbox ) diff --git a/tools/RAiDER/processWM.py b/tools/RAiDER/processWM.py index 4e677cbd2..6bc71b4e1 100755 --- a/tools/RAiDER/processWM.py +++ b/tools/RAiDER/processWM.py @@ -10,10 +10,11 @@ import matplotlib.pyplot as plt import numpy as np - from RAiDER.logger import logger -from RAiDER.models.weatherModel import make_raw_weather_data_filename, checkContainment_raw -from RAiDER.models.customExceptions import * +from RAiDER.models.weatherModel import make_raw_weather_data_filename, checkContainment_raw, make_weather_model_filename +from RAiDER.models.customExceptions import ( + ExistingWeatherModelTooSmall, DatetimeOutsideRange, TryToKeepGoingError, CriticalError +) def prepareWeatherModel( weather_model, From 58832537b3932f531a911ba6a063c5d26b058d44 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 11 Jul 2024 14:34:14 -0500 Subject: [PATCH 03/76] Eliminate comparisons to True or False --- test/test_GUNW.py | 3 +-- test/test_downloadGNSS.py | 4 ++-- tools/RAiDER/cli/statsPlot.py | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/test/test_GUNW.py b/test/test_GUNW.py index d35ae981b..ebe887a65 100644 --- a/test/test_GUNW.py +++ b/test/test_GUNW.py @@ -622,8 +622,7 @@ def test_check_hrrr_availability_all_true(): gunw_id = "S1-GUNW-A-R-106-tops-20220115_20211222-225947-00078W_00041N-PP-4be8-v3_0_0" # Mock _get_acq_time_from_gunw_id to return expected times - result = check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id) - assert result == True + assert check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id) def test_get_slc_ids_from_gunw(): test_path = 'test/gunw_test_data/S1-GUNW-D-R-059-tops-20230320_20220418-180300-00179W_00051N-PP-c92e-v2_0_6.nc' diff --git a/test/test_downloadGNSS.py b/test/test_downloadGNSS.py index e798f1aed..043582418 100644 --- a/test/test_downloadGNSS.py +++ b/test/test_downloadGNSS.py @@ -26,13 +26,13 @@ def test_in_box_inside(): lat = 38.0 lon = -97.0 llbox = [30, 40, -100, -90] # Sample bounding box - assert in_box(lat, lon, llbox) == True + assert in_box(lat, lon, llbox) def test_in_box_outside(): lat = 50.0 lon = -80.0 llbox = [30, 40, -100, -90] # Sample bounding box - assert in_box(lat, lon, llbox) == False + assert not in_box(lat, lon, llbox) # Test fix_lons with various longitudes def test_fix_lons_positive(): diff --git a/tools/RAiDER/cli/statsPlot.py b/tools/RAiDER/cli/statsPlot.py index d75526689..b896a5f9a 100755 --- a/tools/RAiDER/cli/statsPlot.py +++ b/tools/RAiDER/cli/statsPlot.py @@ -970,24 +970,24 @@ def create_DF(self): # e.g. month/day: 03/01 to 06/01 if self.seasonalinterval[0] < self.seasonalinterval[1]: # non leap-year - filtered_self = self.df[(self.df['Date'].dt.is_leap_year == False) & ( + filtered_self = self.df[(not self.df['Date'].dt.is_leap_year) & ( self.df['Date'].dt.dayofyear >= self.seasonalinterval[0]) & (self.df['Date'].dt.dayofyear <= self.seasonalinterval[-1])] # leap-year self.seasonalinterval = [i + 1 if i > 59 else i for i in self.seasonalinterval] - filtered_self_ly = self.df[(self.df['Date'].dt.is_leap_year == True) & ( + filtered_self_ly = self.df[(self.df['Date'].dt.is_leap_year) & ( self.df['Date'].dt.dayofyear >= self.seasonalinterval[0]) & (self.df['Date'].dt.dayofyear <= self.seasonalinterval[-1])] self.df = pd.concat([filtered_self, filtered_self_ly], ignore_index=True) del filtered_self # e.g. month/day: 12/01 to 03/01 if self.seasonalinterval[0] > self.seasonalinterval[1]: # non leap-year - filtered_self = self.df[(self.df['Date'].dt.is_leap_year == False) & ( + filtered_self = self.df[(not self.df['Date'].dt.is_leap_year) & ( self.df['Date'].dt.dayofyear >= self.seasonalinterval[-1]) & (self.df['Date'].dt.dayofyear <= self.seasonalinterval[0])] # leap-year self.seasonalinterval = [i + 1 if i > 59 else i for i in self.seasonalinterval] - filtered_self_ly = self.df[(self.df['Date'].dt.is_leap_year == True) & ( + filtered_self_ly = self.df[(self.df['Date'].dt.is_leap_year) & ( self.df['Date'].dt.dayofyear >= self.seasonalinterval[-1]) & (self.df['Date'].dt.dayofyear <= self.seasonalinterval[0])] self.df = pd.concat([filtered_self, filtered_self_ly], ignore_index=True) del filtered_self From f430f0a3ab8cad7dfb6b2938f357c4c3aa346f65 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:35:13 -0500 Subject: [PATCH 04/76] Add ruff configuration --- pyproject.toml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d5815cdb2..d1998af4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,3 +66,28 @@ multi_line_output = 5 default_section = "THIRDPARTY" [tool.setuptools_scm] + + +[tool.ruff] +line-length = 120 +src = ["tools", "test"] + +[tool.ruff.format] +indent-style = "space" +quote-style = "single" + +[tool.ruff.lint] +extend-select = [ + "I", # isort: https://docs.astral.sh/ruff/rules/#isort-i + "UP", # pyupgrade: https://docs.astral.sh/ruff/rules/#pyupgrade-up + "D", # pydocstyle: https://docs.astral.sh/ruff/rules/#pydocstyle-d + "ANN", # annotations: https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + "PTH", # use-pathlib-pth: https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth +] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.isort] +case-sensitive = true +lines-after-imports = 2 From de5e275f958c3f3755cc2d61b95d2c3f3d3d8d83 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:42:41 -0500 Subject: [PATCH 05/76] Remove unused variables --- tools/RAiDER/losreader.py | 1 - tools/RAiDER/models/ecmwf.py | 2 -- tools/RAiDER/models/gmao.py | 6 ------ tools/RAiDER/models/hrrr.py | 1 - tools/RAiDER/utilFcns.py | 4 ++-- 5 files changed, 2 insertions(+), 12 deletions(-) diff --git a/tools/RAiDER/losreader.py b/tools/RAiDER/losreader.py index a101cab68..15626c389 100644 --- a/tools/RAiDER/losreader.py +++ b/tools/RAiDER/losreader.py @@ -626,7 +626,6 @@ def state_to_los(svs, llh_targets): # Flatten the input array for convenience in_shape = llh_targets[0].shape target_llh = np.stack([x.flatten() for x in llh_targets], axis=-1) - Npts = len(target_llh) # Iterate through targets and compute LOS los_ang, _ = get_radar_pos(target_llh, orb) diff --git a/tools/RAiDER/models/ecmwf.py b/tools/RAiDER/models/ecmwf.py index 2cdf42437..642cb41d4 100755 --- a/tools/RAiDER/models/ecmwf.py +++ b/tools/RAiDER/models/ecmwf.py @@ -199,10 +199,8 @@ def _get_from_cds( if self._model_level_type == 'pl': var = ['z', 'q', 't'] - levType = 'pressure_level' else: var = "129/130/133/152" # 'lnsp', 'q', 'z', 't' - levType = 'model_level' bbox = [lat_max, lon_min, lat_min, lon_max] diff --git a/tools/RAiDER/models/gmao.py b/tools/RAiDER/models/gmao.py index abd0a4a27..62c0574d1 100755 --- a/tools/RAiDER/models/gmao.py +++ b/tools/RAiDER/models/gmao.py @@ -85,12 +85,6 @@ def _fetch(self, out): url = 'https://opendap.nccs.nasa.gov/dods/GEOS-5/fp/0.25_deg/assim/inst3_3d_asm_Nv' session = pydap.cas.urs.setup_session('username', 'password', check_url=url) ds = pydap.client.open_url(url, session=session) - qv = ds['qv'].array[ - time_ind, - ml_min:(ml_max + 1), - lat_min_ind:(lat_max_ind + 1), - lon_min_ind:(lon_max_ind + 1) - ].data[0] p = ds['pl'].array[ time_ind, diff --git a/tools/RAiDER/models/hrrr.py b/tools/RAiDER/models/hrrr.py index 661ae1a56..94248a6cf 100644 --- a/tools/RAiDER/models/hrrr.py +++ b/tools/RAiDER/models/hrrr.py @@ -167,7 +167,6 @@ def load_weather_hrrr(filename): # read data from the netcdf file ds = xarray.open_dataset(filename, engine='netcdf4') # Pull the relevant data from the file - pl = ds.levels.values pres = ds['pres'].values.transpose(1, 2, 0) xArr = ds['x'].values yArr = ds['y'].values diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index c0107928c..a2c5fcbee 100755 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -171,7 +171,7 @@ def rio_open(fname, returnProj=False, userNDV=None, band=None): if band is not None: ndv = nodata[band - 1] data = src.read(band).squeeze() - nodataToNan(data, [userNDV, nodata[band - 1]]) + nodataToNan(data, [userNDV, ndv]) else: data = src.read().squeeze() @@ -536,7 +536,7 @@ def WGS84_to_UTM(lon, lat, common_center=False): if common_center: lon0 = np.median(lon) lat0 = np.median(lat) - z0, l0, x0, y0 = project((lon0, lat0)) + z0, l0, _, _ = project((lon0, lat0)) Z = lon.copy() L = np.zeros(lon.shape, dtype=' Date: Tue, 16 Jul 2024 15:40:27 -0500 Subject: [PATCH 06/76] Ignore D212 D212 is "no line break at the beginning of a docstring". Conforming to it fully will involve more than what the automatic fix for this category does (it disregards that the description must also be short enough to fit on one line), so I'm choosing to have ruff ignore it for now. Remind me and I'll make a separate issue for this. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index d1998af4a..9b1979312 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,7 @@ extend-select = [ "ANN", # annotations: https://docs.astral.sh/ruff/rules/#flake8-annotations-ann "PTH", # use-pathlib-pth: https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth ] +ignore = ["D212"] [tool.ruff.lint.pydocstyle] convention = "google" From d9d8cf75dc99e8ad038cb7cdfab50a368677dc8a Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:52:29 -0500 Subject: [PATCH 07/76] Automatic ruff fixes based on new config Ruff found many more things to change after adding settings to the pyproject.toml. These are the ones it was able to fix automatically. - Imports sorted - Quotes """ used in place of apostrophes ''' for docstrings - Spacing after imports made consistent - Docstring header consistency (headers are title case with an underline of equals signs and no colon at the end) - Use of '{}'.format() replaced with f'{}' where possible - Explicit open() flag 'r' removed as it is the default --- tools/RAiDER/__init__.py | 1 + tools/RAiDER/aria/calcGUNW.py | 19 ++- tools/RAiDER/aria/prepFromGUNW.py | 51 +++--- tools/RAiDER/aws.py | 3 +- tools/RAiDER/checkArgs.py | 17 +- tools/RAiDER/cli/__init__.py | 3 +- tools/RAiDER/cli/__main__.py | 1 - tools/RAiDER/cli/parser.py | 1 + tools/RAiDER/cli/raider.py | 64 ++++---- tools/RAiDER/cli/statsPlot.py | 210 ++++++++++++------------ tools/RAiDER/cli/validators.py | 70 ++++---- tools/RAiDER/constants.py | 1 + tools/RAiDER/delay.py | 18 +- tools/RAiDER/delayFcns.py | 8 +- tools/RAiDER/dem.py | 2 +- tools/RAiDER/getStationDelays.py | 25 ++- tools/RAiDER/gnss/downloadGNSSDelays.py | 71 ++++---- tools/RAiDER/gnss/processDelayFiles.py | 56 +++---- tools/RAiDER/interpolator.py | 26 ++- tools/RAiDER/llreader.py | 53 +++--- tools/RAiDER/losreader.py | 80 ++++----- tools/RAiDER/models/__init__.py | 1 + tools/RAiDER/models/credentials.py | 4 +- tools/RAiDER/models/customExceptions.py | 8 +- tools/RAiDER/models/ecmwf.py | 46 +++--- tools/RAiDER/models/era5.py | 8 +- tools/RAiDER/models/generateGACOSVRT.py | 16 +- tools/RAiDER/models/gmao.py | 22 +-- tools/RAiDER/models/hres.py | 17 +- tools/RAiDER/models/hrrr.py | 42 ++--- tools/RAiDER/models/merra2.py | 24 ++- tools/RAiDER/models/model_levels.py | 4 +- tools/RAiDER/models/ncmr.py | 30 ++-- tools/RAiDER/models/plotWeather.py | 38 +++-- tools/RAiDER/models/template.py | 27 +-- tools/RAiDER/models/weatherModel.py | 179 ++++++++++---------- tools/RAiDER/models/wrf.py | 24 +-- tools/RAiDER/processWM.py | 43 ++--- tools/RAiDER/s1_azimuth_timing.py | 17 +- tools/RAiDER/s1_orbits.py | 2 +- tools/RAiDER/utilFcns.py | 90 +++++----- 41 files changed, 705 insertions(+), 717 deletions(-) diff --git a/tools/RAiDER/__init__.py b/tools/RAiDER/__init__.py index 9c933fb7a..81ece0dba 100644 --- a/tools/RAiDER/__init__.py +++ b/tools/RAiDER/__init__.py @@ -5,6 +5,7 @@ """ from importlib.metadata import version + __version__ = version(__name__) __copyright__ = 'Copyright (c) 2019-2022, California Institute of Technology ("Caltech"). All rights reserved.' diff --git a/tools/RAiDER/aria/calcGUNW.py b/tools/RAiDER/aria/calcGUNW.py index 4c8033db0..b54f802c7 100644 --- a/tools/RAiDER/aria/calcGUNW.py +++ b/tools/RAiDER/aria/calcGUNW.py @@ -3,14 +3,17 @@ Write it to disk """ import os -import xarray as xr -import numpy as np -import RAiDER -from RAiDER.logger import logger from datetime import datetime + import h5py +import numpy as np +import xarray as xr from netCDF4 import Dataset +import RAiDER +from RAiDER.logger import logger + + ## ToDo: # Check difference direction @@ -30,7 +33,7 @@ def compute_delays_slc(cube_filenames: list, wavelength: float) -> xr.Dataset: wavelength : float Depends on sensor, e.g. for Sentinel-1 it is ~.05 - Returns + Returns: ------- xr.Dataset Formatted dataset for GUNW @@ -105,7 +108,7 @@ def compute_delays_slc(cube_filenames: list, wavelength: float) -> xr.Dataset: def update_gunw_slc(path_gunw:str, ds_slc): - """ Update the path_gunw file using the slc delays in ds_slc """ + """Update the path_gunw file using the slc delays in ds_slc""" ## first need to delete the variable; only can seem to with h5 with h5py.File(path_gunw, 'a') as h5: for k in TROPO_GROUP.split(): @@ -175,7 +178,7 @@ def update_gunw_slc(path_gunw:str, ds_slc): def update_gunw_version(path_gunw): - """ temporary hack for updating version to test aria-tools """ + """Temporary hack for updating version to test aria-tools""" with Dataset(path_gunw, mode='a') as ds: ds.version = '1c' return @@ -196,7 +199,7 @@ def tropo_gunw_slc(cube_filenames: list, wavelength : float Wavelength of SAR - Returns + Returns: ------- xr.Dataset Output cube that will be included in GUNW diff --git a/tools/RAiDER/aria/prepFromGUNW.py b/tools/RAiDER/aria/prepFromGUNW.py index 6b6a826fb..b926538e0 100644 --- a/tools/RAiDER/aria/prepFromGUNW.py +++ b/tools/RAiDER/aria/prepFromGUNW.py @@ -6,24 +6,26 @@ # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import os -from datetime import datetime, timezone, timedelta +import sys +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone + import numpy as np -import xarray as xr -import rasterio import pandas as pd -import yaml +import rasterio import shapely.wkt -from dataclasses import dataclass -import sys +import xarray as xr +import yaml from shapely.geometry import box import RAiDER from RAiDER.logger import logger from RAiDER.models import credentials -from RAiDER.models.hrrr import HRRR_CONUS_COVERAGE_POLYGON, AK_GEO, check_hrrr_dataset_availability +from RAiDER.models.hrrr import AK_GEO, HRRR_CONUS_COVERAGE_POLYGON, check_hrrr_dataset_availability from RAiDER.s1_azimuth_timing import get_times_for_azimuth_interpolation from RAiDER.s1_orbits import get_orbits_from_slc_ids_hyp3lib + ## cube spacing in degrees for each model DCT_POSTING = {'HRRR': 0.05, 'HRES': 0.10, 'GMAO': 0.10, 'ERA5': 0.10, 'ERA5T': 0.10, 'MERRA2': 0.1} @@ -54,11 +56,11 @@ def check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id: st ---------- gunw_id : str - Returns + Returns: ------- bool - Example: + Example: check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(S1-GUNW-A-R-106-tops-20220115_20211222-225947-00078W_00041N-PP-4be8-v3_0_0) should return True """ @@ -102,13 +104,14 @@ def check_weather_model_availability(gunw_path: str, gunw_path : str weather_model_name : str Should be one of 'HRRR', 'HRES', 'ERA5', 'ERA5T', 'GMAO', 'MERRA2'. - Returns + + Returns: ------- bool: True if both reference and secondary acquisitions are within the valid range. We assume that reference_date > secondary_date (i.e. reference scenes are most recent) - Raises + Raises: ------ ValueError - If weather model is not correctly referencing the Class from RAiDER.models @@ -176,7 +179,7 @@ def __post_init__(self): def get_bbox(self): - """ Get the bounding box (SNWE) from an ARIA GUNW product """ + """Get the bounding box (SNWE) from an ARIA GUNW product""" with xr.open_dataset(self.path_gunw) as ds: poly_str = ds['productBoundingBox'].data[0].decode('utf-8') @@ -187,14 +190,14 @@ def get_bbox(self): def make_fname(self): - """ Match the ref/sec filename (SLC dates may be different around edge cases) """ + """Match the ref/sec filename (SLC dates may be different around edge cases)""" ref, sec = os.path.basename(self.path_gunw).split('-')[6].split('_') mid_time = os.path.basename(self.path_gunw).split('-')[7] return f'{ref}-{sec}_{mid_time}' def get_datetimes(self): - """ Get the datetimes and set the satellite for orbit """ + """Get the datetimes and set the satellite for orbit""" ref_sec = self.get_slc_dt() middates = [] for aq in ref_sec: @@ -206,7 +209,7 @@ def get_datetimes(self): def get_slc_dt(self): - """ Grab the SLC start date and time from the GUNW """ + """Grab the SLC start date and time from the GUNW""" group = 'science/radarMetaData/inputSLC' lst_sten = [] for i, key in enumerate('reference secondary'.split()): @@ -258,7 +261,7 @@ def get_wavelength(self): def get_orbit_file(self): - """ Get orbit file for reference (GUNW: first & later date)""" + """Get orbit file for reference (GUNW: first & later date)""" orbit_dir = os.path.join(self.out_dir, 'orbits') os.makedirs(orbit_dir, exist_ok=True) @@ -285,7 +288,7 @@ def get_version(self): def getHeights(self): - """ Get the 4 height levels within a GUNW """ + """Get the 4 height levels within a GUNW""" group ='science/grids/imagingGeometry' with xr.open_dataset(self.path_gunw, group=group) as ds: hgts = ds.heightsMeta.data.tolist() @@ -293,7 +296,7 @@ def getHeights(self): def calc_spacing_UTM(self, posting:float=0.01): - """ Convert desired horizontal posting in degrees to meters + """Convert desired horizontal posting in degrees to meters Want to calculate delays close to native model resolution (3 km for HRR) """ @@ -313,7 +316,7 @@ def calc_spacing_UTM(self, posting:float=0.01): def makeLatLonGrid_native(self): - """ Make LatLonGrid at GUNW spacing (90m = 0.00083333º) """ + """Make LatLonGrid at GUNW spacing (90m = 0.00083333º)""" group = 'science/grids/data' with xr.open_dataset(self.path_gunw, group=group) as ds0: lats = ds0.latitude.data @@ -337,7 +340,7 @@ def makeLatLonGrid_native(self): def make_cube(self): - """ Make LatLonGrid at GUNW spacing (90m = 0.00083333º) """ + """Make LatLonGrid at GUNW spacing (90m = 0.00083333º)""" group = 'science/grids/data' with xr.open_dataset(self.path_gunw, group=group) as ds0: lats0 = ds0.latitude.data @@ -358,12 +361,11 @@ def make_cube(self): def update_yaml(dct_cfg:dict, dst:str='GUNW.yaml'): - """ Write a new yaml file from a dictionary. + """Write a new yaml file from a dictionary. Updates parameters in the default 'template.yaml' file. Each key:value pair will in 'dct_cfg' will overwrite that in the default """ - run_config_path = os.path.join( os.path.dirname(RAiDER.__file__), 'cli', @@ -372,7 +374,7 @@ def update_yaml(dct_cfg:dict, dst:str='GUNW.yaml'): 'template.yaml' ) - with open(run_config_path, 'r') as f: + with open(run_config_path) as f: try: params = yaml.safe_load(f) except yaml.YAMLError as exc: @@ -389,8 +391,7 @@ def update_yaml(dct_cfg:dict, dst:str='GUNW.yaml'): def main(args): - """ Read parameters needed for RAiDER from ARIA Standard Products (GUNW) """ - + """Read parameters needed for RAiDER from ARIA Standard Products (GUNW)""" # Check if WEATHER MODEL API credentials hidden file exists, if not create it or raise ERROR credentials.check_api(args.weather_model, args.api_uid, args.api_key) diff --git a/tools/RAiDER/aws.py b/tools/RAiDER/aws.py index 6afb44e5a..23b2334ef 100644 --- a/tools/RAiDER/aws.py +++ b/tools/RAiDER/aws.py @@ -1,11 +1,12 @@ -from typing import Optional, Union from mimetypes import guess_type from pathlib import Path +from typing import Optional, Union import boto3 from RAiDER.logger import logger + S3_CLIENT = boto3.client('s3') diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index b136748a0..1c605b2be 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -6,23 +6,20 @@ # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import os +from datetime import datetime import pandas as pd import rasterio.drivers as rd -from datetime import datetime - - -from RAiDER.losreader import Zenith from RAiDER.logger import logger +from RAiDER.losreader import Zenith def checkArgs(args): - ''' + """ Helper fcn for checking argument compatibility and returns the correct variables - ''' - + """ ######################################################################################################################### # Directories if args.weather_model_directory is None: @@ -111,11 +108,11 @@ def get_raster_ext(fmt): try: return extensions[fmt.upper()] except KeyError: - raise ValueError('{} is not a valid gdal/rasterio file format for rasters'.format(fmt)) + raise ValueError(f'{fmt} is not a valid gdal/rasterio file format for rasters') def makeDelayFileNames(time, los, outformat, weather_model_name, out): - ''' + """ return names for the wet and hydrostatic delays. # Examples: @@ -123,7 +120,7 @@ def makeDelayFileNames(time, los, outformat, weather_model_name, out): ('some_dir/model_name_wet_00_00_00_ztd.h5', 'some_dir/model_name_hydro_00_00_00_ztd.h5') >>> makeDelayFileNames(None, None, "h5", "model_name", "some_dir") ('some_dir/model_name_wet_ztd.h5', 'some_dir/model_name_hydro_ztd.h5') - ''' + """ format_string = "{model_name}_{{}}_{time}{los}.{ext}".format( model_name=weather_model_name, time=time.strftime("%Y%m%dT%H%M%S_") if time is not None else "", diff --git a/tools/RAiDER/cli/__init__.py b/tools/RAiDER/cli/__init__.py index 0368c77ee..1cab33255 100644 --- a/tools/RAiDER/cli/__init__.py +++ b/tools/RAiDER/cli/__init__.py @@ -1,4 +1,5 @@ -from RAiDER.constants import _ZREF, _CUBE_SPACING_IN_M +from RAiDER.constants import _CUBE_SPACING_IN_M, _ZREF + class AttributeDict(dict): __getattr__ = dict.__getitem__ diff --git a/tools/RAiDER/cli/__main__.py b/tools/RAiDER/cli/__main__.py index 6e3401ebe..a6bf1e73d 100644 --- a/tools/RAiDER/cli/__main__.py +++ b/tools/RAiDER/cli/__main__.py @@ -1,6 +1,5 @@ import argparse import sys - from importlib.metadata import entry_points import RAiDER.cli.conf as conf diff --git a/tools/RAiDER/cli/parser.py b/tools/RAiDER/cli/parser.py index e340f3cce..53ffb7e0b 100644 --- a/tools/RAiDER/cli/parser.py +++ b/tools/RAiDER/cli/parser.py @@ -3,6 +3,7 @@ from RAiDER.cli.validators import BBoxAction, IntegerMappingType + def add_cpus(parser: argparse.ArgumentParser): parser.add_argument( '--cpus', diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 9f1646e85..8f6aaf08a 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -1,30 +1,31 @@ import argparse import datetime -import os import json +import os import shutil import sys -import yaml +from pathlib import Path +from textwrap import dedent import numpy as np import xarray as xr +import yaml -from textwrap import dedent -from pathlib import Path - -import RAiDER.aria.prepFromGUNW import RAiDER.aria.calcGUNW +import RAiDER.aria.prepFromGUNW from RAiDER import aws -from RAiDER.logger import logger, logging from RAiDER.cli import DEFAULT_DICT, AttributeDict -from RAiDER.cli.parser import add_out, add_cpus, add_verbose +from RAiDER.cli.parser import add_cpus, add_out, add_verbose from RAiDER.cli.validators import DateListAction, date_type +from RAiDER.logger import logger, logging from RAiDER.models.allowed import ALLOWED_MODELS -from RAiDER.models.customExceptions import ( - NoWeatherModelData, DatetimeFailed, TryToKeepGoingError, WrongNumberOfFiles +from RAiDER.models.customExceptions import DatetimeFailed, NoWeatherModelData, TryToKeepGoingError, WrongNumberOfFiles +from RAiDER.s1_azimuth_timing import ( + get_inverse_weights_for_dates, + get_s1_azimuth_time_grid, + get_times_for_azimuth_interpolation, ) from RAiDER.utilFcns import get_dt -from RAiDER.s1_azimuth_timing import get_s1_azimuth_time_grid, get_inverse_weights_for_dates, get_times_for_azimuth_interpolation TIME_INTERPOLATION_METHODS = ['none', 'center_time', 'azimuth_time_grid'] @@ -49,6 +50,7 @@ def read_run_config_file(fname): """ Read the run config file into a dictionary structure. + Args: fname (str): full path to the run config file Returns: @@ -58,16 +60,14 @@ def read_run_config_file(fname): >>> run_config = read_run_config_file('raider.yaml') """ - from RAiDER.cli.validators import ( - enforce_time, parse_dates, get_query_region, get_heights, get_los, enforce_wm - ) - with open(fname, 'r') as f: + from RAiDER.cli.validators import enforce_time, enforce_wm, get_heights, get_los, get_query_region, parse_dates + with open(fname) as f: try: params = yaml.safe_load(f) except yaml.YAMLError as exc: print(exc) raise ValueError( - 'Something is wrong with the yaml file {}'.format(fname)) + f'Something is wrong with the yaml file {fname}') # Drop any values not specified params = drop_nans(params) @@ -139,12 +139,12 @@ def drop_nans(d): def calcDelays(iargs=None): - """ Parse command line arguments using argparse. """ + """Parse command line arguments using argparse.""" import RAiDER import RAiDER.processWM - from RAiDER.delay import tropo_delay from RAiDER.checkArgs import checkArgs - from RAiDER.utilFcns import writeDelays, get_nearest_wmtimes + from RAiDER.delay import tropo_delay + from RAiDER.utilFcns import get_nearest_wmtimes, writeDelays examples = 'Examples of use:' \ '\n\t raider.py run_config_file.yaml' \ '\n\t raider.py --generate_config template' @@ -305,7 +305,7 @@ def calcDelays(iargs=None): ) logger.info(f'Query datetime: {tt}') logger.error(e) - logger.error('Weather model files are: {}'.format(wfiles)) + logger.error(f'Weather model files are: {wfiles}') logger.error( f'Downloading and/or preparation of {model._Name} failed.' ) @@ -624,10 +624,10 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: # ------------------------------------------------------------ processDelays.py def combineZTDFiles(): - ''' + """ Command-line program to process delay files from RAiDER and GNSS into a single file. - ''' - from RAiDER.gnss.processDelayFiles import main, combineDelayFiles, create_parser + """ + from RAiDER.gnss.processDelayFiles import combineDelayFiles, create_parser, main p = create_parser() args = p.parse_args() @@ -651,14 +651,13 @@ def combineZTDFiles(): def getWeatherFile(wfiles, times, t, model, interp_method='none'): - ''' + """ # Time interpolation # # Need to handle various cases, including if the exact weather model time is # requested, or if one or more datetimes are not available from the weather # model data provider - ''' - + """ # time interpolation method: number of expected files EXPECTED_NUM_FILES = {'none': 1, 'center_time': 2, 'azimuth_time_grid': 3} @@ -669,7 +668,7 @@ def getWeatherFile(wfiles, times, t, model, interp_method='none'): Nfiles_expected = EXPECTED_NUM_FILES[interp_method] except KeyError: raise ValueError( - 'getWeatherFile: interp_method {} is not known'.format(interp_method)) + f'getWeatherFile: interp_method {interp_method} is not known') Nmatch = (Nfiles_expected == Nfiles) Tmatch = (Nfiles == Ntimes) @@ -725,8 +724,7 @@ def getWeatherFile(wfiles, times, t, model, interp_method='none'): def combine_weather_files(wfiles, t, model, interp_method='center_time'): - '''Interpolate downloaded weather files and save to a single file''' - + """Interpolate downloaded weather files and save to a single file""" STYLE = {'center_time': '_timeInterp_', 'azimuth_time_grid': '_timeInterpAziGrid_'} @@ -771,8 +769,7 @@ def combine_weather_files(wfiles, t, model, interp_method='center_time'): def combine_files_using_azimuth_time(wfiles, t, times): - '''Combine files using azimuth time interpolation''' - + """Combine files using azimuth time interpolation""" # read the individual datetime datasets datasets = [xr.open_dataset(f) for f in wfiles] @@ -809,7 +806,7 @@ def combine_files_using_azimuth_time(wfiles, t, times): def get_weights_time_interp(times, t): - '''Calculate weights for time interpolation using simple inverse linear weighting''' + """Calculate weights for time interpolation using simple inverse linear weighting""" date1, date2 = times wgts = [1 - get_dt(t, date1) / get_dt(date2, date1), 1 - get_dt(date2, t) / get_dt(date2, date1)] @@ -825,8 +822,7 @@ def get_weights_time_interp(times, t): def get_time_grid_for_aztime_interp(datasets, t, model): - '''Calculate the time-varying grid for use with azimuth time interpolation''' - + """Calculate the time-varying grid for use with azimuth time interpolation""" # Each model will require some inspection here # the subsequent s1 azimuth time grid requires dimension # inputs to all have same dimensions and either be diff --git a/tools/RAiDER/cli/statsPlot.py b/tools/RAiDER/cli/statsPlot.py index b896a5f9a..bfcf856b0 100755 --- a/tools/RAiDER/cli/statsPlot.py +++ b/tools/RAiDER/cli/statsPlot.py @@ -5,18 +5,6 @@ # RESERVED. United States Government Sponsorship acknowledged. # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -from RAiDER.logger import logger, logging -from RAiDER.cli.parser import add_cpus -from RAiDER.utilFcns import WGS84_to_UTM -from rasterio.transform import Affine -from scipy import optimize -from scipy.optimize import OptimizeWarning -from shapely.strtree import STRtree -from shapely.geometry import Point, Polygon -from matplotlib import pyplot as plt -import pandas as pd -import numpy as np -import rasterio import argparse import copy import datetime as dt @@ -26,6 +14,21 @@ import warnings import matplotlib as mpl +import numpy as np +import pandas as pd +import rasterio +from matplotlib import pyplot as plt +from rasterio.transform import Affine +from scipy import optimize +from scipy.optimize import OptimizeWarning +from shapely.geometry import Point, Polygon +from shapely.strtree import STRtree + +from RAiDER.cli.parser import add_cpus +from RAiDER.logger import logger, logging +from RAiDER.utilFcns import WGS84_to_UTM + + # must switch to Agg to avoid multiprocessing crashes mpl.use('Agg') @@ -169,10 +172,9 @@ def cmd_line_parse(iargs=None): def convert_SI(val, unit_in, unit_out): - ''' - Convert input to desired units - ''' - + """ + Convert input to desired units + """ SI = {'mm': 0.001, 'cm': 0.01, 'm': 1.0, 'km': 1000., 'mm^2': 1e-6, 'cm^2': 1e-4, 'm^2': 1.0, 'km^2': 1e+6} @@ -181,21 +183,21 @@ def convert_SI(val, unit_in, unit_out): # adjust if input isn't datetime, and assume it to be part of workflow # e.g. sigZTD filter, already extracted datetime object try: - return eval('val.apply(pd.to_datetime).dt.{}.astype(float).astype("Int32")'.format(unit_out)) + return eval(f'val.apply(pd.to_datetime).dt.{unit_out}.astype(float).astype("Int32")') except AttributeError: return val # check if output spatial unit is supported if unit_out not in SI: - raise ValueError("User-specified output unit {} not recognized.".format(unit_out)) + raise ValueError(f"User-specified output unit {unit_out} not recognized.") return val * SI[unit_in] / SI[unit_out] def midpoint(p1, p2): - ''' - Calculate central longitude for '--time_lines' option - ''' + """ + Calculate central longitude for '--time_lines' option + """ import math if p1[1] == p2[1]: @@ -213,9 +215,9 @@ def midpoint(p1, p2): def save_gridfile(df, gridfile_type, fname, plotbbox, spacing, unit, colorbarfmt='%.2f', stationsongrids=False, time_lines=False, dtype="float32", noData=np.nan): - ''' - Function to save gridded-arrays as GDAL-readable file. - ''' + """ + Function to save gridded-arrays as GDAL-readable file. + """ # Pass metadata metadata_dict = {} metadata_dict['gridfile_type'] = gridfile_type @@ -249,10 +251,9 @@ def save_gridfile(df, gridfile_type, fname, plotbbox, spacing, unit, def load_gridfile(fname, unit): - ''' - Function to load gridded-arrays saved from previous runs. - ''' - + """ + Function to load gridded-arrays saved from previous runs. + """ try: with rasterio.open(fname) as src: grid_array = src.read(1).astype(float) @@ -305,10 +306,10 @@ def load_gridfile(fname, unit): return grid_array, plotbbox, spacing, colorbarfmt, stationsongrids, time_lines -class VariogramAnalysis(): - ''' - Class which ingests dataframe output from 'RaiderStats' class and performs variogram analysis. - ''' +class VariogramAnalysis: + """ + Class which ingests dataframe output from 'RaiderStats' class and performs variogram analysis. + """ def __init__(self, filearg, gridpoints, col_name, unit='m', workdir='./', seasonalinterval=None, densitythreshold=10, binnedvariogram=False, numCPUs=8, variogram_per_timeslice=False, variogram_errlimit='inf'): self.df = filearg @@ -324,9 +325,9 @@ def __init__(self, filearg, gridpoints, col_name, unit='m', workdir='./', season self.variogram_errlimit = float(variogram_errlimit) def _get_samples(self, data, Nsamp=1000): - ''' + """ pull samples from a 2D image for variogram analysis - ''' + """ import random if len(data) < self.densitythreshold: logger.warning('Less than {} points for this gridcell', self.densitythreshold) @@ -346,32 +347,32 @@ def _get_samples(self, data, Nsamp=1000): return d, indpars def _get_XY(self, x2d, y2d, indpars): - ''' + """ Given a list of indices, return the x,y locations from two matrices - ''' + """ x = np.array([[x2d[r[0]], x2d[r[1]]] for r in indpars]) y = np.array([[y2d[r[0]], y2d[r[1]]] for r in indpars]) return x, y def _get_distances(self, XY): - ''' + """ Return the distances between each point in a list of points - ''' + """ from scipy.spatial.distance import cdist return np.diag(cdist(XY[:, :, 0], XY[:, :, 1], metric='euclidean')) def _get_variogram(self, XY, xy=None): - ''' + """ Return variograms - ''' + """ return 0.5 * np.square(XY - xy) # XY = 1st col xy= 2nd col def _emp_vario(self, x, y, data, Nsamp=1000): - ''' + """ Compute empirical semivariance - ''' + """ # remove NaNs if possible mask = ~np.isnan(data) if False in mask: @@ -394,9 +395,9 @@ def _emp_vario(self, x, y, data, Nsamp=1000): return dists, vario def _binned_vario(self, hEff, rawVario, xBin=None): - ''' + """ return a binned empirical variogram - ''' + """ if xBin is None: with warnings.catch_warnings(): warnings.filterwarnings("ignore", message="All-NaN slice encountered") @@ -424,9 +425,9 @@ def _binned_vario(self, hEff, rawVario, xBin=None): return np.array(hExp), np.array(expVario) def _fit_vario(self, dists, vario, model=None, x0=None, Nparm=None, ub=None): - ''' + """ Fit a variogram model to data - ''' + """ from scipy.optimize import least_squares def resid(x, d, v, m): @@ -466,10 +467,10 @@ def resid(x, d, v, m): # this would be expontential plus nugget def __exponential__(self, parms, h, nugget=False): - ''' + """ returns a variogram model given a set of arguments and key-word arguments - ''' + """ # a = range, b = sill, c = nugget model a, b, c = parms with warnings.catch_warnings(): @@ -481,16 +482,16 @@ def __exponential__(self, parms, h, nugget=False): # this would be gaussian plus nugget def __gaussian__(self, parms, h): - ''' + """ returns a Gaussian variogram model - ''' + """ a, b, c = parms return b * (1 - np.exp(-np.square(h) / (a**2))) + c def _append_variogram(self, grid_ind, grid_subset): - ''' + """ For a given grid-cell, iterate through time slices to generate/append empirical variogram(s) - ''' + """ # Comprehensive arrays recording data across all time epochs for given station dists_arr = [] vario_arr = [] @@ -505,9 +506,7 @@ def _append_variogram(self, grid_ind, grid_subset): # Record skipped [gridnode, timeslice] self.skipped_slices.append([grid_ind, j.strftime("%Y-%m-%d")]) else: - self.gridcenterlist.append(['grid{} '.format( - grid_ind) + 'Lat:{} Lon:{}'.format( - str(self.gridpoints[grid_ind][1]), str(self.gridpoints[grid_ind][0]))]) + self.gridcenterlist.append([f'grid{grid_ind} ' + f'Lat:{str(self.gridpoints[grid_ind][1])} Lon:{str(self.gridpoints[grid_ind][0])}']) lonarr = np.array( grid_subset[grid_subset['Date'] == j]['Lon']) latarr = np.array( @@ -522,18 +521,18 @@ def _append_variogram(self, grid_ind, grid_subset): res_robust, d_test, v_test = self._fit_vario( dists_binned, vario_binned, model=self.__exponential__, x0=None, Nparm=3) # Plot empirical + experimental variogram for this gridnode and timeslice - if not os.path.exists(os.path.join(self.workdir, 'variograms/grid{}'.format(grid_ind))): + if not os.path.exists(os.path.join(self.workdir, f'variograms/grid{grid_ind}')): os.makedirs(os.path.join( - self.workdir, 'variograms/grid{}'.format(grid_ind))) + self.workdir, f'variograms/grid{grid_ind}')) # Make variogram plots for each time-slice if self.variogram_per_timeslice: # Plot empirical variogram for this gridnode and timeslice self.plot_variogram(grid_ind, j.strftime("%Y%m%d"), [self.gridpoints[grid_ind][1], self.gridpoints[grid_ind][0]], - workdir=os.path.join(self.workdir, 'variograms/grid{}'.format(grid_ind)), dists=dists, vario=vario, + workdir=os.path.join(self.workdir, f'variograms/grid{grid_ind}'), dists=dists, vario=vario, dists_binned=dists_binned, vario_binned=vario_binned) # Plot experimental variogram for this gridnode and timeslice self.plot_variogram(grid_ind, j.strftime("%Y%m%d"), [self.gridpoints[grid_ind][1], self.gridpoints[grid_ind][0]], - workdir=os.path.join(self.workdir, 'variograms/grid{}'.format(grid_ind)), d_test=d_test, v_test=v_test, + workdir=os.path.join(self.workdir, f'variograms/grid{grid_ind}'), d_test=d_test, v_test=v_test, res_robust=res_robust.x, dists_binned=dists_binned, vario_binned=vario_binned) # append for plotting self.good_slices.append([grid_ind, j.strftime("%Y%m%d")]) @@ -571,11 +570,11 @@ def _append_variogram(self, grid_ind, grid_subset): self.TOT_res_robust_rmse.append(np.array(np.nan)) # Plot empirical variogram for this gridnode self.plot_variogram(grid_ind, tot_timetag, [self.gridpoints[grid_ind][1], self.gridpoints[grid_ind][0]], - workdir=os.path.join(self.workdir, 'variograms/grid{}'.format(grid_ind)), dists=dists_arr, vario=vario_arr, + workdir=os.path.join(self.workdir, f'variograms/grid{grid_ind}'), dists=dists_arr, vario=vario_arr, dists_binned=dists_binned_arr, vario_binned=vario_binned_arr, seasonalinterval=self.seasonalinterval) # Plot experimental variogram for this gridnode self.plot_variogram(grid_ind, tot_timetag, [self.gridpoints[grid_ind][1], self.gridpoints[grid_ind][0]], - workdir=os.path.join(self.workdir, 'variograms/grid{}'.format(grid_ind)), d_test=TOT_d_test, v_test=TOT_v_test, + workdir=os.path.join(self.workdir, f'variograms/grid{grid_ind}'), d_test=TOT_d_test, v_test=TOT_v_test, res_robust=TOT_res_robust.x, seasonalinterval=self.seasonalinterval, dists_binned=dists_binned_arr, vario_binned=vario_binned_arr) # Record sparse grids which didn't have sufficient sample size of data through any of the timeslices else: @@ -584,9 +583,9 @@ def _append_variogram(self, grid_ind, grid_subset): return self.TOT_good_slices, self.TOT_res_robust_arr, self.TOT_res_robust_rmse, self.gridcenterlist def create_variograms(self): - ''' + """ Iterate through grid-cells and time slices to generate empirical variogram(s) - ''' + """ # track data for plotting self.TOT_good_slices = [] self.TOT_res_robust_arr = [] @@ -627,16 +626,15 @@ def create_variograms(self): return TOT_grids, self.TOT_res_robust_arr, self.TOT_res_robust_rmse def plot_variogram(self, gridID, timeslice, coords, workdir='./', d_test=None, v_test=None, res_robust=None, dists=None, vario=None, dists_binned=None, vario_binned=None, seasonalinterval=None): - ''' + """ Make empirical and/or experimental variogram fit plots - ''' + """ # If specified workdir doesn't exist, create it if not os.path.exists(workdir): os.mkdir(workdir) # make plot title - title_str = ' \nLat:{:.2f} Lon:{:.2f}\nTime:{}'.format( - coords[1], coords[0], str(timeslice)) + title_str = f' \nLat:{coords[1]:.2f} Lon:{coords[0]:.2f}\nTime:{str(timeslice)}' if seasonalinterval: title_str += ' Season(mm/dd): {}/{} – {}/{}'.format(int(timeslice[4:6]), int( timeslice[6:8]), int(timeslice[-4:-2]), int(timeslice[-2:])) @@ -651,17 +649,17 @@ def plot_variogram(self, gridID, timeslice, coords, workdir='./', d_test=None, v plt.plot(dists_binned, vario_binned, 'bo', label='binned') if res_robust is not None: plt.axhline(y=res_robust[1], color='g', - linestyle='--', label='ɣ\u0332\u00b2({}\u00b2)'.format(self.unit)) + linestyle='--', label=f'ɣ\u0332\u00b2({self.unit}\u00b2)') # scale from m to user-defined units res_robust[0] = convert_SI(res_robust[0], 'm', self.unit) plt.axvline(x=res_robust[0], color='c', - linestyle='--', label='h ({})'.format(self.unit)) + linestyle='--', label=f'h ({self.unit})') if d_test is not None and v_test is not None: # scale from m to user-defined units d_test = [convert_SI(i, 'm', self.unit) for i in d_test] plt.plot(d_test, v_test, 'r-', label='experimental fit') - plt.xlabel('Distance ({})'.format(self.unit)) - plt.ylabel('Dissimilarity ({}\u00b2)'.format(self.unit)) + plt.xlabel(f'Distance ({self.unit})') + plt.ylabel(f'Dissimilarity ({self.unit}\u00b2)') plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0., framealpha=1.) # Plot empirical variogram @@ -669,22 +667,22 @@ def plot_variogram(self, gridID, timeslice, coords, workdir='./', d_test=None, v plt.title('Empirical variogram' + title_str) plt.tight_layout() plt.savefig(os.path.join( - workdir, 'grid{}_timeslice{}_justEMPvariogram.eps'.format(gridID, timeslice))) + workdir, f'grid{gridID}_timeslice{timeslice}_justEMPvariogram.eps')) # Plot just experimental variogram else: plt.title('Experimental variogram' + title_str) plt.tight_layout() plt.savefig(os.path.join( - workdir, 'grid{}_timeslice{}_justEXPvariogram.eps'.format(gridID, timeslice))) + workdir, f'grid{gridID}_timeslice{timeslice}_justEXPvariogram.eps')) plt.close() return -class RaiderStats(object): - ''' - Class which loads standard weather model/GPS delay files and generates a series of user-requested statistics and graphics. - ''' +class RaiderStats: + """ + Class which loads standard weather model/GPS delay files and generates a series of user-requested statistics and graphics. + """ # import dependencies import glob @@ -752,8 +750,7 @@ def __init__(self, filearg, col_name, unit='m', workdir='./', bbox=None, spacing if self.colorpercentile is None: self.colorpercentile = [25, 95] if self.colorpercentile[0] > self.colorpercentile[1]: - raise Exception('Input colorpercentile lower threshold {} higher than upper threshold {}'.format( - self.colorpercentile[0], self.colorpercentile[1])) + raise Exception(f'Input colorpercentile lower threshold {self.colorpercentile[0]} higher than upper threshold {self.colorpercentile[1]}') # load dataframe directly if previously generated TIF grid-file if self.fname.endswith('.tif'): @@ -834,7 +831,7 @@ def __init__(self, filearg, col_name, unit='m', workdir='./', bbox=None, spacing self.create_DF() def _get_extent(self): # dataset, spacing=1, userbbox=None - """ Get the bbox, spacing in deg (by default 1deg), optionally pass user-specified bbox. Output array in WESN degrees """ + """Get the bbox, spacing in deg (by default 1deg), optionally pass user-specified bbox. Output array in WESN degrees""" extent = [np.floor(min(self.df['Lon'])), np.ceil(max(self.df['Lon'])), np.floor(min(self.df['Lat'])), np.ceil(max(self.df['Lat']))] if self.bbox is not None: @@ -891,10 +888,10 @@ def _get_extent(self): # dataset, spacing=1, userbbox=None return extent, grid_dim, gridpoints def _check_stationgrid_intersection(self, stat_ID): - ''' + """ Return index of grid cell which intersects with station Note: Fast, but assumes station locations don't change - ''' + """ coord = Point((self.unique_points[1][self.unique_points[0].index( stat_ID)], self.unique_points[2][self.unique_points[0].index(stat_ID)])) # Get grid cell polygon which intersect with station coordinate @@ -906,9 +903,9 @@ def _check_stationgrid_intersection(self, stat_ID): return 'NaN' def _reader(self): - ''' + """ Read a input file - ''' + """ try: data = pd.read_csv(self.fname, parse_dates=['Datetime']) data['Date'] = data['Datetime'].apply(lambda x: x.date()) @@ -919,11 +916,11 @@ def _reader(self): # check if user-specified key is valid if self.col_name not in data.keys(): raise Exception( - 'User-specified key {} not found in input file {}. Must specify valid key.' .format(self.col_name, self.fname)) + f'User-specified key {self.col_name} not found in input file {self.fname}. Must specify valid key.' ) # if user-specified key is the same as the 'Date' field, rename if self.col_name == 'Date': - logger.warning('Input key {} same as "Date" field name, rename the former'.format(self.col_name)) + logger.warning(f'Input key {self.col_name} same as "Date" field name, rename the former') self.col_name += '_plot' data[self.col_name] = data['Date'] @@ -941,9 +938,9 @@ def _reader(self): return data def create_DF(self): - ''' - Create dataframe. - ''' + """ + Create dataframe. + """ # Open file self.df = self._reader() @@ -1196,8 +1193,7 @@ def create_DF(self): self.df['phsfit'] = self.df['ID'].map(self.phsfit) # check if there are any valid data values if self.df['phsfit'].isnull().values.all(axis=0): - raise Exception("No valid data values, adjust --min_span inputs for time span in years {} and/or fractional obs. {}". - format(self.min_span[0], self.min_span[1])) + raise Exception(f"No valid data values, adjust --min_span inputs for time span in years {self.min_span[0]} and/or fractional obs. {self.min_span[1]}") self.df['ampfit'] = self.df['ID'].map(self.ampfit) self.df['periodfit'] = self.df['ID'].map(self.periodfit) self.phsfit_c = {k: v for d in self.phsfit_c for k, v in d.items()} @@ -1394,13 +1390,13 @@ def create_DF(self): time_lines=self.time_lines, dtype='float32') def _amplitude_and_phase(self, station, tt, yy, min_span=2, min_frac=0.6, period_limit=0.): - ''' + """ Fit sin to the input time sequence, and return fitting parameters: "amp", "omega", "phase", "offset", "freq", "period" and "fitfunc". Minimum time span in years (min_span), minimum fractional observations in span (min_frac), and period limit (period_limit) enforced for statistical analysis. Source: https://stackoverflow.com/questions/16716302/how-do-i-fit-a-sine-curve-to-my-data-with-pylab-and-numpy - ''' + """ ampfit = {} phsfit = {} periodfit = {} @@ -1460,7 +1456,7 @@ def custom_sine_function_base(t, A, w, p, c): warnings.simplefilter("ignore", OptimizeWarning) popt, pcov = optimize.curve_fit(custom_sine_function_base, tt, yy, p0=guess, maxfev=int(1e6)) print('OptimizeWarning: Covariance for station {} could not be estimated. Refer to debug figure here {} \ - '.format(station, os.path.join(self.workdir, 'phaseamp_per_station', 'station{}.png'.format(station)))) + '.format(station, os.path.join(self.workdir, 'phaseamp_per_station', f'station{station}.png'))) pass # Adjust expected output to reflect fixed period, if specified if period_limit != 0.: @@ -1504,7 +1500,7 @@ def fitfunc(t): tt_plot /= 31556952 plt.plot(tt_plot, yy, "ok", label="input") plt.xlabel("time (years)") - plt.ylabel("data ({})".format(self.unit)) + plt.ylabel(f"data ({self.unit})") num_testpoints = len(tt) * 10 if num_testpoints > 1000: num_testpoints = 1000 @@ -1517,7 +1513,7 @@ def fitfunc(t): plt.legend(loc="best") if not os.path.exists(os.path.join(self.workdir, 'phaseamp_per_station')): os.mkdir(os.path.join(self.workdir, 'phaseamp_per_station')) - plt.savefig(os.path.join(self.workdir, 'phaseamp_per_station', 'station{}.png'.format(station)), + plt.savefig(os.path.join(self.workdir, 'phaseamp_per_station', f'station{station}.png'), format='png', bbox_inches='tight') plt.close() optimize_warning = False @@ -1534,15 +1530,15 @@ def fitfunc(t): self.phsfit_c, self.periodfit_c, self.seasonalfit_rmse def _sine_function_base(self, t, A, w, p, c): - ''' + """ Base function for modeling sinusoidal amplitude/phase fits. - ''' + """ return A * np.sin(w * t + p) + c def __call__(self, gridarr, plottype, workdir='./', drawgridlines=False, colorbarfmt='%.2f', stationsongrids=None, resValue=5, plotFormat='pdf', userTitle=None): - ''' - Visualize a suite of statistics w.r.t. stations. Pass either a list of points or a gridded array as the first argument. Alternatively, you may superimpose your gridded array with a supplementary list of points by passing the latter through the stationsongrids argument. - ''' + """ + Visualize a suite of statistics w.r.t. stations. Pass either a list of points or a gridded array as the first argument. Alternatively, you may superimpose your gridded array with a supplementary list of points by passing the latter through the stationsongrids argument. + """ from cartopy import crs as ccrs from cartopy import feature as cfeature from cartopy.mpl.ticker import LatitudeFormatter, LongitudeFormatter @@ -1701,7 +1697,7 @@ def __call__(self, gridarr, plottype, workdir='./', drawgridlines=False, colorba if 'cbar_ax' in locals(): # experimental variogram fit sill heatmap if plottype == "grid_variance": - cbar_ax.set_label(" ".join(plottype.replace('grid_', '').split('_')).title() + ' ({}\u00b2)'.format(self.unit), + cbar_ax.set_label(" ".join(plottype.replace('grid_', '').split('_')).title() + f' ({self.unit}\u00b2)', rotation=-90, labelpad=10) # specify appropriate units for mean/median/std/amplitude/experimental variogram fit heatmap elif plottype == "grid_delay_mean" or plottype == "grid_delay_median" or plottype == "grid_delay_stdev" or \ @@ -1715,10 +1711,10 @@ def __call__(self, gridarr, plottype, workdir='./', drawgridlines=False, colorba # update label if sigZTD if 'sig' in self.col_name: cbar_ax.set_label("sig ZTD " + " ".join(plottype.replace('grid_', - '').replace('delay_', '').split('_')).title() + ' ({})'.format(self.unit), + '').replace('delay_', '').split('_')).title() + f' ({self.unit})', rotation=-90, labelpad=10) else: - cbar_ax.set_label(" ".join(plottype.replace('grid_', '').split('_')).title() + ' ({})'.format(self.unit), + cbar_ax.set_label(" ".join(plottype.replace('grid_', '').split('_')).title() + f' ({self.unit})', rotation=-90, labelpad=10) # specify appropriate units for phase heatmap (days) elif plottype == "station_seasonal_phase" or plottype == "grid_seasonal_phase" or plottype == "grid_seasonal_absolute_phase" or \ @@ -1795,10 +1791,10 @@ def stats_analyses( variogram_per_timeslice, variogram_errlimit ): - ''' + """ Main workflow for generating a suite of plots to illustrate spatiotemporal distribution and/or character of zenith delays - ''' + """ if verbose: logger.setLevel(logging.DEBUG) @@ -2002,7 +1998,7 @@ def stats_analyses( if unit in ['minute', 'hour', 'day', 'year']: unit = 'm' df_stats.unit = 'm' - logger.warning("Output unit {} specified for Variogram analysis. Reverted to meters".format(unit)) + logger.warning(f"Output unit {unit} specified for Variogram analysis. Reverted to meters") make_variograms = VariogramAnalysis(df_stats.df, df_stats.gridpoints, col_name, unit, workdir, df_stats.seasonalinterval, densitythreshold, binnedvariogram, numCPUs, variogram_per_timeslice, variogram_errlimit) diff --git a/tools/RAiDER/cli/validators.py b/tools/RAiDER/cli/validators.py index b25c0cf50..35ece1e8e 100755 --- a/tools/RAiDER/cli/validators.py +++ b/tools/RAiDER/cli/validators.py @@ -1,21 +1,20 @@ -from argparse import Action, ArgumentError, ArgumentTypeError - import importlib import itertools import os import re - -import pandas as pd -import numpy as np - -from datetime import time, timedelta, datetime, date +from argparse import Action, ArgumentError, ArgumentTypeError +from datetime import date, datetime, time, timedelta from textwrap import dedent from time import strptime -from RAiDER.llreader import BoundingBox, RasterRDR, StationFile, GeocodedFile, Geocube -from RAiDER.losreader import Zenith, Conventional -from RAiDER.utilFcns import rio_extents, rio_profile +import numpy as np +import pandas as pd + +from RAiDER.llreader import BoundingBox, GeocodedFile, Geocube, RasterRDR, StationFile from RAiDER.logger import logger +from RAiDER.losreader import Conventional, Zenith +from RAiDER.utilFcns import rio_extents, rio_profile + _BUFFER_SIZE = 0.2 # default buffer size in lat/lon degrees @@ -25,10 +24,10 @@ def enforce_wm(value, aoi): _, model_obj = modelName2Module(model) except ModuleNotFoundError: raise NotImplementedError( - dedent(''' - Model {} is not yet fully implemented, + dedent(f''' + Model {model} is not yet fully implemented, please contribute! - '''.format(model)) + ''') ) ## check the user requsted bounding box is within the weather model domain @@ -64,9 +63,9 @@ def get_los(args): def get_heights(args, out, station_file, bounding_box=None): - ''' + """ Parse the Height info and download a DEM if needed - ''' + """ dem_path = out out = { @@ -125,9 +124,9 @@ def get_heights(args, out, station_file, bounding_box=None): def get_query_region(args): - ''' + """ Parse the query region from inputs - ''' + """ # Get bounds from the inputs # make sure this is first if args.get('use_dem_latlon'): @@ -197,10 +196,9 @@ def enforce_bbox(bbox): def parse_dates(arg_dict): - ''' + """ Determine the requested dates from the input parameters - ''' - + """ if arg_dict.get('date_list'): l = arg_dict['date_list'] if isinstance(l, str): @@ -251,14 +249,14 @@ def enforce_valid_dates(arg): raise ValueError( - 'Unable to coerce {} to a date. Try %Y-%m-%d'.format(arg) + f'Unable to coerce {arg} to a date. Try %Y-%m-%d' ) def enforce_time(arg_dict): - ''' + """ Parse an input time (required to be ISO 8601) - ''' + """ try: arg_dict['time'] = convert_time(arg_dict['time']) except KeyError: @@ -325,9 +323,9 @@ def modelName2Module(model_name): def getBufferedExtent(lats, lons=None, buf=0.): - ''' + """ get the bounding box around a set of lats/lons - ''' + """ if lons is None: lats, lons = lats[..., 0], lons[..., 1] @@ -350,11 +348,11 @@ def getBufferedExtent(lats, lons=None, buf=0.): def isOutside(extent1, extent2): - ''' + """ Determine whether any of extent1 lies outside extent2 extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon] Equal extents are considered "inside" - ''' + """ t1 = extent1[0] < extent2[0] t2 = extent1[1] > extent2[1] t3 = extent1[2] < extent2[2] @@ -365,11 +363,11 @@ def isOutside(extent1, extent2): def isInside(extent1, extent2): - ''' + """ Determine whether all of extent1 lies inside extent2 extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon]. Equal extents are considered "inside" - ''' + """ t1 = extent1[0] <= extent2[0] t2 = extent1[1] >= extent2[1] t3 = extent1[2] <= extent2[2] @@ -398,11 +396,11 @@ def date_type(arg): pass raise ArgumentTypeError( - 'Unable to coerce {} to a date. Try %Y-%m-%d'.format(arg) + f'Unable to coerce {arg} to a date. Try %Y-%m-%d' ) -class MappingType(object): +class MappingType: """ A type that maps arguments to constants. @@ -431,15 +429,13 @@ def __call__(self, arg): if self._default is self.UNSET: raise KeyError( - "Invalid choice '{}', must be one of {}".format( - arg, list(self.mapping.keys()) - ) + f"Invalid choice '{arg}', must be one of {list(self.mapping.keys())}" ) return self._default -class IntegerType(object): +class IntegerType: """ A type that converts arguments to integers. @@ -460,9 +456,9 @@ def __call__(self, arg): integer = int(arg) if self.lo is not None and integer < self.lo: - raise ArgumentTypeError("Must be greater than {}".format(self.lo)) + raise ArgumentTypeError(f"Must be greater than {self.lo}") if self.hi is not None and integer > self.hi: - raise ArgumentTypeError("Must be less than {}".format(self.hi)) + raise ArgumentTypeError(f"Must be less than {self.hi}") return integer diff --git a/tools/RAiDER/constants.py b/tools/RAiDER/constants.py index 00673c1bc..67066e9e3 100644 --- a/tools/RAiDER/constants.py +++ b/tools/RAiDER/constants.py @@ -7,6 +7,7 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import numpy as np + _ZMIN = np.float64(-100) # minimum required height _ZREF = np.float64(26000) # maximum integration height when not specified by user _STEP = np.float64(15.0) # integration step size in meters diff --git a/tools/RAiDER/delay.py b/tools/RAiDER/delay.py index bd1097baf..9c311f512 100755 --- a/tools/RAiDER/delay.py +++ b/tools/RAiDER/delay.py @@ -13,20 +13,20 @@ "wet_total" and "hydro_total" fields specified. """ import os -import pyproj -import xarray - from datetime import datetime, timezone -from pyproj import CRS, Transformer from typing import List, Union import numpy as np +import pyproj +import xarray +from pyproj import CRS, Transformer from RAiDER.constants import _ZREF from RAiDER.delayFcns import getInterpolators from RAiDER.logger import logger from RAiDER.losreader import build_ray + ############################################################################### def tropo_delay( dt, @@ -332,9 +332,9 @@ def _build_cube_ray( def writeResultsToXarray(dt, xpts, ypts, zpts, crs, wetDelay, hydroDelay, weather_model_file, out_type): - ''' + """ write a 1-D array to a NETCDF5 file - ''' + """ # Modify this as needed for NISAR / other projects ds = xarray.Dataset( data_vars=dict( @@ -370,7 +370,7 @@ def writeResultsToXarray(dt, xpts, ypts, zpts, crs, wetDelay, hydroDelay, weathe ) # Write projection system mapping - ds["crs"] = int(-2147483647) # dummy placeholder + ds["crs"] = -2147483647 # dummy placeholder for k, v in crs.to_cf().items(): ds.crs.attrs[k] = v @@ -405,7 +405,7 @@ def writeResultsToXarray(dt, xpts, ypts, zpts, crs, wetDelay, hydroDelay, weathe def transformPoints(lats: np.ndarray, lons: np.ndarray, hgts: np.ndarray, old_proj: CRS, new_proj: CRS) -> np.ndarray: - ''' + """ Transform lat/lon/hgt data to an array of points in a new projection @@ -418,7 +418,7 @@ def transformPoints(lats: np.ndarray, lons: np.ndarray, hgts: np.ndarray, old_pr Returns: ndarray: the array of query points in the weather model coordinate system (YX) - ''' + """ # Flags for flipping inputs or outputs if not isinstance(new_proj, CRS): new_proj = CRS.from_epsg(new_proj.lstrip('EPSG:')) diff --git a/tools/RAiDER/delayFcns.py b/tools/RAiDER/delayFcns.py index 22f8485db..99e68c0d4 100755 --- a/tools/RAiDER/delayFcns.py +++ b/tools/RAiDER/delayFcns.py @@ -10,22 +10,20 @@ except ImportError: mp = None -import xarray - import numpy as np - +import xarray from scipy.interpolate import RegularGridInterpolator as Interpolator from RAiDER.logger import logger def getInterpolators(wm_file, kind='pointwise', shared=False): - ''' + """ Read 3D gridded data from a processed weather model file and wrap it with the scipy RegularGridInterpolator The interpolator grid is (y, x, z) - ''' + """ # Get the weather model data try: ds = xarray.load_dataset(wm_file) diff --git a/tools/RAiDER/dem.py b/tools/RAiDER/dem.py index ef4bdd403..ecceacae7 100644 --- a/tools/RAiDER/dem.py +++ b/tools/RAiDER/dem.py @@ -8,7 +8,6 @@ import os import numpy as np - import rasterio from dem_stitcher.stitcher import stitch_dem @@ -25,6 +24,7 @@ def download_dem( ): """ Download a DEM if one is not already present. + Args: llbounds: list/ndarry of floats -lat/lon bounds of the area to download. Values should be ordered in the following way: [S, N, W, E] writeDEM: boolean -write the DEM to file diff --git a/tools/RAiDER/getStationDelays.py b/tools/RAiDER/getStationDelays.py index f61f8b8db..f0c9d65f9 100644 --- a/tools/RAiDER/getStationDelays.py +++ b/tools/RAiDER/getStationDelays.py @@ -20,7 +20,7 @@ def get_delays_UNR(stationFile, filename, dateList, returnTime=None): - ''' + """ Parses and returns a dictionary containing either (1) all the GPS delays, if returnTime is None, or (2) only the delay at the closest times to to returnTime. @@ -59,7 +59,7 @@ def get_delays_UNR(stationFile, filename, dateList, returnTime=None): as are the formal error columns (_SIG). Source —> http://geodesy.unr.edu/gps_timeseries/README_trop2.txt) - ''' + """ # sort through station zip files allstationTarfiles = [] # if URL @@ -98,12 +98,12 @@ def get_delays_UNR(stationFile, filename, dateList, returnTime=None): split_lines = line.split() # units: mm, mm, mm, deg, deg, deg, deg, mm, mm, K trotot, trototSD, trwet, tgetot, tgetotSD, tgntot, tgntotSD, wvapor, wvaporSD, mtemp = \ - [float(t) for t in split_lines[2:]] + (float(t) for t in split_lines[2:]) except BaseException: # TODO: What error(s)? continue site = split_lines[0] - year, doy, seconds = [int(n) - for n in split_lines[1].split(':')] + year, doy, seconds = (int(n) + for n in split_lines[1].split(':')) # Break iteration if time from line in file does not match date reported in filename if doy != doyFromFile: logger.warning( @@ -175,9 +175,9 @@ def get_delays_UNR(stationFile, filename, dateList, returnTime=None): def get_station_data(inFile, dateList, gps_repo=None, numCPUs=8, outDir=None, returnTime=None): - ''' + """ Pull tropospheric delay data for a given station name - ''' + """ if outDir is None: outDir = os.getcwd() @@ -222,7 +222,7 @@ def get_station_data(inFile, dateList, gps_repo=None, numCPUs=8, outDir=None, re # Consolidate all CSV files into one object if outputfiles == []: raise Exception('No valid delays found for specified time/region.') - name = os.path.join(outDir, '{}combinedGPS_ztd.csv'.format(gps_repo)) + name = os.path.join(outDir, f'{gps_repo}combinedGPS_ztd.csv') statsFile = pd.concat([pd.read_csv(i) for i in outputfiles]) # drop all duplicate lines statsFile.drop_duplicates(inplace=True) @@ -244,10 +244,9 @@ def get_station_data(inFile, dateList, gps_repo=None, numCPUs=8, outDir=None, re def get_date(stationFile): - ''' + """ extract the date from a station delay file - ''' - + """ # find the date info year = int(stationFile[1]) doy = int(stationFile[2]) @@ -257,9 +256,9 @@ def get_date(stationFile): def seconds_of_day(returnTime): - ''' + """ Convert HH:MM:SS format time-tag to seconds of day. - ''' + """ if isinstance(returnTime, dt.time): h, m, s = returnTime.hour, returnTime.minute, returnTime.second else: diff --git a/tools/RAiDER/gnss/downloadGNSSDelays.py b/tools/RAiDER/gnss/downloadGNSSDelays.py index 1713ce022..c27f9dbc4 100755 --- a/tools/RAiDER/gnss/downloadGNSSDelays.py +++ b/tools/RAiDER/gnss/downloadGNSSDelays.py @@ -8,12 +8,14 @@ import itertools import multiprocessing import os + import pandas as pd -from RAiDER.logger import logger, logging from RAiDER.getStationDelays import get_station_data -from RAiDER.utilFcns import requests_retry_session +from RAiDER.logger import logger, logging from RAiDER.models.customExceptions import NoStationDataFoundError +from RAiDER.utilFcns import requests_retry_session + # base URL for UNR repository _UNR_URL = "http://geodesy.unr.edu/" @@ -26,7 +28,7 @@ def get_station_list( name_appendix='', writeStationFile=True, ): - ''' + """ Creates a list of stations inside a lat/lon bounding box from a source Args: @@ -40,7 +42,7 @@ def get_station_list( Returns: stations: list of strings - station IDs to access output_file: string or dataframe - file to write delays - ''' + """ if bbox is not None: station_data = get_stats_by_llh(llhBox=bbox) else: @@ -48,7 +50,7 @@ def get_station_list( station_data = pd.read_csv(stationFile) except: stations = [] - with open(stationFile, 'r') as f: + with open(stationFile) as f: for k, line in enumerate(f): if k ==0: names = line.strip().split() @@ -68,10 +70,10 @@ def get_station_list( def get_stats_by_llh(llhBox=None, baseURL=_UNR_URL): - ''' + """ Function to pull lat, lon, height, beginning date, end date, and number of solutions for stations inside the bounding box llhBox. llhBox should be a tuple in SNWE format. - ''' + """ if llhBox is None: llhBox = [-90, 90, 0, 360] S, N, W, E = llhBox @@ -79,7 +81,7 @@ def get_stats_by_llh(llhBox=None, baseURL=_UNR_URL): raise ValueError( 'get_stats_by_llh: bounding box must be on lon range [0, 360]') - stationHoldings = '{}NGLStationPages/llh.out'.format(baseURL) + stationHoldings = f'{baseURL}NGLStationPages/llh.out' # it's a file like object and works just like a file stations = pd.read_csv( @@ -103,7 +105,7 @@ def download_tropo_delays( numCPUs=8, download=False, ): - ''' + """ Check for and download GNSS tropospheric delays from an archive. If download is True then files will be physically downloaded, but this is not necessary as data can be virtually accessed. @@ -118,8 +120,7 @@ def download_tropo_delays( Returns: None - ''' - + """ # argument checking if not isinstance(stats, (list, str)): raise TypeError('stats should be a string or a list of strings') @@ -141,7 +142,7 @@ def download_tropo_delays( ] else: raise NotImplementedError( - 'download_tropo_delays: gps_repo "{}" not yet implemented'.format(gps_repo)) + f'download_tropo_delays: gps_repo "{gps_repo}" not yet implemented') # Write results to file if len(results) == 0: @@ -149,25 +150,25 @@ def download_tropo_delays( station_list=stats['ID'].to_list(), years=years) statDF = pd.DataFrame(results).set_index('ID') statDF.to_csv(os.path.join( - writeDir, '{}gnssStationList_overbbox_withpaths.csv'.format(gps_repo))) + writeDir, f'{gps_repo}gnssStationList_overbbox_withpaths.csv')) def download_UNR(statID, year, writeDir='.', download=False, baseURL=_UNR_URL): - ''' + """ Download a zip file containing tropospheric delays for a given station and year The URL format is http://geodesy.unr.edu/gps_timeseries/trop//..trop.zip Inputs: statID - 4-character station identifier year - 4-numeral year - ''' + """ if baseURL not in [_UNR_URL]: - raise NotImplementedError('Data repository {} has not yet been implemented'.format(baseURL)) + raise NotImplementedError(f'Data repository {baseURL} has not yet been implemented') URL = "{0}gps_timeseries/trop/{1}/{1}.{2}.trop.zip".format( baseURL, statID.upper(), year) logger.debug('Currently checking station %s in %s', statID, year) if download: - saveLoc = os.path.abspath(os.path.join(writeDir, '{0}.{1}.trop.zip'.format(statID.upper(), year))) + saveLoc = os.path.abspath(os.path.join(writeDir, f'{statID.upper()}.{year}.trop.zip')) filepath = download_url(URL, saveLoc) if filepath == '': raise ValueError('Year or station ID does not exist') @@ -177,10 +178,10 @@ def download_UNR(statID, year, writeDir='.', download=False, baseURL=_UNR_URL): def download_url(url, save_path, chunk_size=2048): - ''' + """ Download a file from a URL. Modified from https://stackoverflow.com/questions/9419162/download-returned-zip-file-from-url - ''' + """ session = requests_retry_session() r = session.get(url, stream=True) @@ -196,10 +197,10 @@ def download_url(url, save_path, chunk_size=2048): def check_url(url): - ''' + """ Check whether a file exists at a URL. Modified from https://stackoverflow.com/questions/9419162/download-returned-zip-file-from-url - ''' + """ session = requests_retry_session() r = session.head(url) if r.status_code == 404: @@ -208,9 +209,9 @@ def check_url(url): def in_box(lat, lon, llhbox): - ''' + """ Checks whether the given lat, lon pair are inside the bounding box llhbox - ''' + """ return lat < llhbox[1] and lat > llhbox[0] and lon < llhbox[3] and lon > llhbox[2] @@ -226,9 +227,9 @@ def fix_lons(lon): def get_ID(line): - ''' + """ Pulls the station ID, lat, lon, and height for a given entry in the UNR text file - ''' + """ stat_id, lat, lon, height = line.split()[:4] return stat_id, float(lat), float(lon), float(height) @@ -277,16 +278,16 @@ def main(inps=None): ) # Combine station data with URL info - pathsdf = pd.read_csv(os.path.join(out, '{}gnssStationList_overbbox_withpaths.csv'.format(gps_repo))) + pathsdf = pd.read_csv(os.path.join(out, f'{gps_repo}gnssStationList_overbbox_withpaths.csv')) pathsdf = pd.merge(left=pathsdf, right=statdf, how='left', left_on='ID', right_on='ID') - pathsdf.to_csv(os.path.join(out, '{}gnssStationList_overbbox_withpaths.csv'.format(gps_repo)), index=False) + pathsdf.to_csv(os.path.join(out, f'{gps_repo}gnssStationList_overbbox_withpaths.csv'), index=False) del statdf, pathsdf # Extract delays for each station dateList = [k.strftime('%Y-%m-%d') for k in dateList] get_station_data( os.path.join( - out, '{}gnssStationList_overbbox_withpaths.csv'.format(gps_repo)), + out, f'{gps_repo}gnssStationList_overbbox_withpaths.csv'), dateList, gps_repo=gps_repo, numCPUs=cpus, @@ -298,9 +299,9 @@ def main(inps=None): def parse_bbox(bounding_box): - ''' + """ Parse bounding box arguments - ''' + """ if isinstance(bounding_box, str) and not os.path.isfile(bounding_box): try: bbox = [float(val) for val in bounding_box.split()] @@ -327,9 +328,9 @@ def parse_bbox(bounding_box): def get_stats(bbox, long_cross_zero, out, station_file): - ''' + """ Pull the stations needed - ''' + """ if long_cross_zero == 1: bbox1 = bbox.copy() bbox2 = bbox.copy() @@ -356,7 +357,7 @@ def get_stats(bbox, long_cross_zero, out, station_file): def filterToBBox(stations, llhBox): - ''' + """ Filter a dataframe by lat/lon. *NOTE: llhBox longitude format should be [0, 360] @@ -366,7 +367,7 @@ def filterToBBox(stations, llhBox): Returns: a Pandas Dataframe with stations removed that are not inside llhBox - ''' + """ S, N, W, E = llhBox if (W < 0) or (E < 0): raise ValueError('llhBox longitude format should 0-360') @@ -388,7 +389,7 @@ def filterToBBox(stations, llhBox): if stations[lon_key].min() < 0: # convert lon format to -180 to 180 - W, E = [((D + 180) % 360) - 180 for D in [W, E]] + W, E = (((D + 180) % 360) - 180 for D in [W, E]) mask = (stations[lat_key] > S) & (stations[lat_key] < N) & ( stations[lon_key] < E) & (stations[lon_key] > W) diff --git a/tools/RAiDER/gnss/processDelayFiles.py b/tools/RAiDER/gnss/processDelayFiles.py index c4a5ec076..e6fe5814f 100644 --- a/tools/RAiDER/gnss/processDelayFiles.py +++ b/tools/RAiDER/gnss/processDelayFiles.py @@ -1,14 +1,15 @@ -from textwrap import dedent import argparse import datetime import glob +import math import os import re -import math +from textwrap import dedent +import pandas as pd from tqdm import tqdm -import pandas as pd + pd.options.mode.chained_assignment = None # default='warn' @@ -33,7 +34,7 @@ def combineDelayFiles(outName, loc=os.getcwd(), source='model', ext='.csv', ref= files.to_csv(outName, index=False) return - print('Combining {} delay files'.format(source)) + print(f'Combining {source} delay files') try: concatDelayFiles( files, @@ -53,8 +54,7 @@ def combineDelayFiles(outName, loc=os.getcwd(), source='model', ext='.csv', ref= def addDateTimeToFiles(fileList, force=False, verbose=False): - ''' Run through a list of files and add the datetime of each file as a column ''' - + """Run through a list of files and add the datetime of each file as a column""" print('Adding Datetime to delay files') for f in tqdm(fileList): @@ -63,9 +63,9 @@ def addDateTimeToFiles(fileList, force=False, verbose=False): if 'Datetime' in data.columns and not force: if verbose: print( - 'File {} already has a "Datetime" column, pass' + f'File {f} already has a "Datetime" column, pass' '"force = True" if you want to override and ' - 're-process'.format(f) + 're-process' ) else: try: @@ -78,14 +78,14 @@ def addDateTimeToFiles(fileList, force=False, verbose=False): data.to_csv(f, index=False) except (AttributeError, ValueError): print( - 'File {} does not contain datetime info, skipping' - .format(f) + f'File {f} does not contain datetime info, skipping' + ) del data def getDateTime(filename): - ''' Parse a datetime from a RAiDER delay filename ''' + """Parse a datetime from a RAiDER delay filename""" filename = os.path.basename(filename) dtr = re.compile(r'\d{8}T\d{6}') dt = dtr.search(filename) @@ -96,7 +96,7 @@ def getDateTime(filename): def update_time(row, localTime_hrs): - '''Update with local origin time''' + """Update with local origin time""" localTime_estimate = row['Datetime'].replace(hour=localTime_hrs, minute=0, second=0) # determine if you need to shift days @@ -121,7 +121,7 @@ def update_time(row, localTime_hrs): def pass_common_obs(reference, target, localtime=None): - '''Pass only observations in target spatiotemporally common to reference''' + """Pass only observations in target spatiotemporally common to reference""" if isinstance(target['Datetime'].iloc[0], str): target['Datetime'] = target['Datetime'].apply(lambda x: datetime.datetime.strptime(x, '%Y-%m-%d %H:%M:%S')) @@ -145,10 +145,10 @@ def concatDelayFiles( ref=None, col_name='ZTD' ): - ''' + """ Read a list of .csv files containing the same columns and append them together, sorting by specified columns - ''' + """ dfList = [] print('Concatenating delay files') @@ -171,10 +171,8 @@ def concatDelayFiles( ).drop_duplicates().reset_index(drop=True) df_c.sort_values(by=sort_list, inplace=True) - print('Total number of rows in the concatenated file: {}'.format(df_c.shape[0])) - print('Total number of rows containing NaNs: {}'.format( - df_c[df_c.isna().any(axis=1)].shape[0] - ) + print(f'Total number of rows in the concatenated file: {df_c.shape[0]}') + print(f'Total number of rows containing NaNs: {df_c[df_c.isna().any(axis=1)].shape[0]}' ) if return_df or outName is None: @@ -188,9 +186,9 @@ def concatDelayFiles( def local_time_filter(raiderFile, ztdFile, dfr, dfz, localTime): - ''' + """ Convert to local-time reference frame WRT 0 longitude - ''' + """ localTime_hrs = int(localTime.split(' ')[0]) localTime_hrthreshold = int(localTime.split(' ')[1]) # with rotation rate and distance to 0 lon, get localtime shift WRT 00 UTC at 0 lon @@ -245,9 +243,9 @@ def local_time_filter(raiderFile, ztdFile, dfr, dfz, localTime): def readZTDFile(filename, col_name='ZTD'): - ''' + """ Read and parse a GPS zenith delay file - ''' + """ try: data = pd.read_csv(filename, parse_dates=['Date']) times = data['times'].apply(lambda x: datetime.timedelta(seconds=x)) @@ -358,10 +356,10 @@ def create_parser(): def main(raiderFile, ztdFile, col_name='ZTD', raider_delay='totalDelay', outName=None, localTime=None): - ''' + """ Merge a combined RAiDER delays file with a GPS ZTD delay file - ''' - print('Merging delay files {} and {}'.format(raiderFile, ztdFile)) + """ + print(f'Merging delay files {raiderFile} and {ztdFile}') dfr = pd.read_csv(raiderFile, parse_dates=['Datetime']) # drop extra columns expected_data_columns = ['ID', 'Lat', 'Lon', 'Hgt_m', 'Datetime', 'wetDelay', @@ -419,10 +417,8 @@ def main(raiderFile, ztdFile, col_name='ZTD', raider_delay='totalDelay', outName dfc['ZTD_minus_RAiDER'] = dfc['ZTD'] - dfc[raider_delay] print('Total number of rows in the concatenated file: ' - '{}'.format(dfc.shape[0])) - print('Total number of rows containing NaNs: {}'.format( - dfc[dfc.isna().any(axis=1)].shape[0] - ) + f'{dfc.shape[0]}') + print(f'Total number of rows containing NaNs: {dfc[dfc.isna().any(axis=1)].shape[0]}' ) print('Merge finished') diff --git a/tools/RAiDER/interpolator.py b/tools/RAiDER/interpolator.py index eda923c16..4098c12c6 100644 --- a/tools/RAiDER/interpolator.py +++ b/tools/RAiDER/interpolator.py @@ -7,13 +7,12 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import numpy as np import pandas as pd - from scipy.interpolate import interp1d from RAiDER.interpolate import interpolate -class RegularGridInterpolator(object): +class RegularGridInterpolator: """ Provides a wrapper around RAiDER.interpolate.interpolate with a similar interface to scipy.interpolate.RegularGridInterpolator. @@ -59,14 +58,14 @@ def __call__(self, points): def interp_along_axis(oldCoord, newCoord, data, axis=2, pad=False): - ''' + """ DEPRECATED: Use RAiDER.interpolate.interpolate_along_axis instead (it is much faster). This function now primarily exists to verify the behavior of the new one. Interpolate an array of 3-D data along one axis. This function assumes that the x-coordinate increases monotonically. - ''' + """ if oldCoord.ndim > 1: stackedData = np.concatenate([oldCoord, data, newCoord], axis=axis) out = np.apply_along_axis(interpVector, axis=axis, arr=stackedData, Nx=oldCoord.shape[axis]) @@ -78,18 +77,18 @@ def interp_along_axis(oldCoord, newCoord, data, axis=2, pad=False): def interpV(y, old_x, new_x, left=None, right=None, period=None): - ''' + """ Rearrange np.interp's arguments - ''' + """ return np.interp(new_x, old_x, y, left=left, right=right, period=period) def interpVector(vec, Nx): - ''' + """ Interpolate data from a single vector containing the original x, the original y, and the new x, in that order. Nx tells the number of original x-points. - ''' + """ x = vec[:Nx] y = vec[Nx:2 * Nx] xnew = vec[2 * Nx:] @@ -98,17 +97,16 @@ def interpVector(vec, Nx): def fillna3D(array, axis=-1, fill_value=0.): - ''' + """ This function fills in NaNs in 3D arrays, specifically using the nearest non-nan value for "low" NaNs and 0s for "high" NaNs. - Arguments: + Arguments: array - 3D array, where the last axis is the "z" dimension - Returns: + Returns: 3D array with low NaNs filled as nearest neighbors and high NaNs filled as 0s - ''' - + """ # fill lower NaNs with nearest neighbor narr = np.moveaxis(array, axis, -1) nars = narr.reshape((np.prod(narr.shape[:-1]),) + (narr.shape[-1],)) @@ -122,7 +120,7 @@ def fillna3D(array, axis=-1, fill_value=0.): def interpolateDEM(demFile, outLL, method='nearest'): - """ Interpolate a DEM raster to a set of lat/lon query points using rioxarray + """Interpolate a DEM raster to a set of lat/lon query points using rioxarray outLL will be a tuple of (lats, lons). lats/lons can either be 1D arrays or 2 For now will only use first row/col of 2D diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index a92b59745..452087228 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -6,10 +6,11 @@ # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import os + +import numpy as np import pyproj import xarray -import numpy as np try: import pandas as pd @@ -18,19 +19,19 @@ from pyproj import CRS -from RAiDER.utilFcns import rio_open, rio_stats from RAiDER.logger import logger +from RAiDER.utilFcns import rio_open, rio_stats -class AOI(object): - ''' +class AOI: + """ This instantiates a generic AOI class object. Attributes: _bounding_box - S N W E bounding box _proj - pyproj-compatible CRS _type - Type of AOI - ''' + """ def __init__(self): self._output_directory = os.getcwd() self._bounding_box = None @@ -56,7 +57,7 @@ def projection(self): def get_output_spacing(self, crs=4326): - """ Return the output spacing in desired units """ + """Return the output spacing in desired units""" output_spacing_deg = self._output_spacing if not isinstance(crs, CRS): crs = CRS.from_epsg(crs) @@ -71,7 +72,7 @@ def get_output_spacing(self, crs=4326): def set_output_spacing(self, ll_res=None): - """ Calculate the spacing for the output grid and weather model + """Calculate the spacing for the output grid and weather model Use the requested spacing if exists or the weather model grid itself @@ -178,7 +179,7 @@ def set_output_directory(self, output_directory): def set_output_xygrid(self, dst_crs=4326): - """ Define the locations where the delays will be returned """ + """Define the locations where the delays will be returned""" from RAiDER.utilFcns import transform_bbox try: @@ -201,7 +202,7 @@ def set_output_xygrid(self, dst_crs=4326): class StationFile(AOI): - '''Use a .csv file containing at least Lat, Lon, and optionally Hgt_m columns''' + """Use a .csv file containing at least Lat, Lon, and optionally Hgt_m columns""" def __init__(self, station_file, demFile=None): super().__init__() self._filename = station_file @@ -211,15 +212,15 @@ def __init__(self, station_file, demFile=None): def readLL(self): - '''Read the station lat/lons from the csv file''' + """Read the station lat/lons from the csv file""" df = pd.read_csv(self._filename).drop_duplicates(subset=["Lat", "Lon"]) return df['Lat'].values, df['Lon'].values def readZ(self): - ''' + """ Read the station heights from the file, or download a DEM if not present - ''' + """ df = pd.read_csv(self._filename).drop_duplicates(subset=["Lat", "Lon"]) if 'Hgt_m' in df.columns: return df['Hgt_m'].values @@ -251,9 +252,9 @@ def readZ(self): class RasterRDR(AOI): - ''' + """ Use a 2-band raster file containing lat/lon coordinates. - ''' + """ def __init__(self, lat_file, lon_file=None, hgt_file=None, dem_file=None, convention='isce'): super().__init__() self._type = 'radar_rasters' @@ -289,9 +290,9 @@ def readLL(self): def readZ(self): - ''' + """ Read the heights from the raster file, or download a DEM if not present - ''' + """ if self._hgtfile is not None and os.path.exists(self._hgtfile): logger.info('Using existing heights at: %s', self._hgtfile) return rio_open(self._hgtfile) @@ -315,7 +316,7 @@ def readZ(self): class BoundingBox(AOI): - '''Parse a bounding box AOI''' + """Parse a bounding box AOI""" def __init__(self, bbox): AOI.__init__(self) self._bounding_box = bbox @@ -323,11 +324,11 @@ def __init__(self, bbox): class GeocodedFile(AOI): - '''Parse a Geocoded file for coordinates''' + """Parse a Geocoded file for coordinates""" def __init__(self, filename, is_dem=False): super().__init__() - from RAiDER.utilFcns import rio_profile, rio_extents + from RAiDER.utilFcns import rio_extents, rio_profile self._filename = filename self.p = rio_profile(filename) @@ -354,9 +355,9 @@ def readLL(self): def readZ(self): - ''' + """ Download a DEM for the file - ''' + """ from RAiDER.dem import download_dem from RAiDER.interpolator import interpolateDEM @@ -369,7 +370,7 @@ def readZ(self): class Geocube(AOI): - """ Pull lat/lon/height from a georeferenced data cube """ + """Pull lat/lon/height from a georeferenced data cube""" def __init__(self, path_cube): super().__init__() self.path = path_cube @@ -398,10 +399,10 @@ def readZ(self): def bounds_from_latlon_rasters(latfile, lonfile): - ''' + """ Parse lat/lon/height inputs and return the appropriate outputs - ''' + """ from RAiDER.utilFcns import get_file_and_band latinfo = get_file_and_band(latfile) loninfo = get_file_and_band(lonfile) @@ -426,10 +427,10 @@ def bounds_from_latlon_rasters(latfile, lonfile): def bounds_from_csv(station_file): - ''' + """ station_file should be a comma-delimited file with at least "Lat" and "Lon" columns, which should be EPSG: 4326 projection (i.e WGS84) - ''' + """ stats = pd.read_csv(station_file).drop_duplicates(subset=["Lat", "Lon"]) snwe = [stats['Lat'].min(), stats['Lat'].max(), stats['Lon'].min(), stats['Lon'].max()] return snwe diff --git a/tools/RAiDER/losreader.py b/tools/RAiDER/losreader.py index 15626c389..4a05394c1 100644 --- a/tools/RAiDER/losreader.py +++ b/tools/RAiDER/losreader.py @@ -6,14 +6,16 @@ # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -import os import datetime +import os import shelve from abc import ABC -from typing import Union from pathlib import PosixPath +from typing import Union import numpy as np + + try: import xml.etree.ElementTree as ET except ImportError: @@ -24,15 +26,13 @@ isce = None from RAiDER.constants import _ZREF -from RAiDER.utilFcns import ( - cosd, sind, rio_open, lla2ecef, ecef2lla -) +from RAiDER.utilFcns import cosd, ecef2lla, lla2ecef, rio_open, sind class LOS(ABC): - ''' + """ LOS Class definition for handling look vectors - ''' + """ def __init__(self): self._lats, self._lons, self._heights = None, None, None self._look_vecs = None @@ -42,7 +42,7 @@ def __init__(self): def setPoints(self, lats, lons=None, heights=None): - '''Set the pixel locations''' + """Set the pixel locations""" if (lats is None) and (self._lats is None): raise RuntimeError("You haven't given any point locations yet") @@ -88,7 +88,7 @@ def __init__(self): def setLookVectors(self): - '''Set point locations and calculate Zenith look vectors''' + """Set point locations and calculate Zenith look vectors""" if self._lats is None: raise ValueError('Target points not set') if self._look_vecs is None: @@ -96,7 +96,7 @@ def setLookVectors(self): def __call__(self, delays): - '''Placeholder method for consistency with the other classes''' + """Placeholder method for consistency with the other classes""" return delays @@ -117,9 +117,9 @@ def __init__(self, filename=None, los_convention='isce', time=None, pad=600): def __call__(self, delays): - ''' + """ Read the LOS file and convert it to look vectors - ''' + """ if self._lats is None: raise ValueError('Target points not set') if self._file is None: @@ -183,7 +183,7 @@ class Raytracing(LOS): """ def __init__(self, filename=None, los_convention='isce', time=None, look_dir = 'right', pad=600): - '''read in and parse a statevector file''' + """Read in and parse a statevector file""" if isce is None: raise ImportError('isce3 is required for this class. Use conda to install isce3`') @@ -234,9 +234,9 @@ def setTime(self, time, pad=600): def getLookVectors(self, ht, llh, xyz, yy): - ''' + """ Calculate look vectors for raytracing - ''' + """ if isce is None: raise ImportError('isce3 is required for this method. Use conda to install isce3`') @@ -304,10 +304,10 @@ def getIntersectionWithLevels(self, levels): def calculateDelays(self, delays): - ''' + """ Here "delays" is point-wise delays (i.e. refractivities), not integrated ZTD/STD. - ''' + """ # Create rays (Use getIntersectionWithLevels above) # Interpolate delays to rays # Integrate along rays @@ -316,7 +316,7 @@ def calculateDelays(self, delays): def getZenithLookVecs(lats, lons, heights): - ''' + """ Returns look vectors when Zenith is used. Args: @@ -324,7 +324,7 @@ def getZenithLookVecs(lats, lons, heights): Returns: zenLookVecs (ndarray): - (in_shape) x 3 unit look vectors in an ECEF reference frame - ''' + """ x = np.cos(np.radians(lats)) * np.cos(np.radians(lons)) y = np.cos(np.radians(lats)) * np.sin(np.radians(lons)) z = np.sin(np.radians(lats)) @@ -392,7 +392,7 @@ def filter_ESA_orbit_file_p(path: str) -> bool: def inc_hd_to_enu(incidence, heading): - ''' + """ Convert incidence and heading to line-of-sight vectors from the ground to the top of the troposphere. @@ -405,7 +405,7 @@ def inc_hd_to_enu(incidence, heading): LOS: ndarray - (input_shape) x 3 array of unit look vectors in local ENU Algorithm referenced from http://earthdef.caltech.edu/boards/4/topics/327 - ''' + """ if np.any(incidence < 0): raise ValueError('inc_hd_to_enu: Incidence angle cannot be less than 0') @@ -447,7 +447,7 @@ def read_shelve(filename): def read_txt_file(filename): - ''' + """ Read a 7-column text file containing orbit statevectors. Time should be denoted as integer time in seconds since the reference epoch (user-requested time). @@ -461,7 +461,7 @@ def read_txt_file(filename): Returns: svs (list): - a length-7 list of numpy vectors containing the above variables - ''' + """ t = list() x = list() y = list() @@ -469,17 +469,17 @@ def read_txt_file(filename): vx = list() vy = list() vz = list() - with open(filename, 'r') as f: + with open(filename) as f: for line in f: try: parts = line.strip().split() t_ = datetime.datetime.fromisoformat(parts[0]) - x_, y_, z_, vx_, vy_, vz_ = [float(t) for t in parts[1:]] + x_, y_, z_, vx_, vy_, vz_ = (float(t) for t in parts[1:]) except ValueError: raise ValueError( - "I need {} to be a 7 column text file, with ".format(filename) + + f"I need {filename} to be a 7 column text file, with " + "columns t, x, y, z, vx, vy, vz (Couldn't parse line " + - "{})".format(repr(line))) + f"{repr(line)})") t.append(t_) x.append(x_) y.append(y_) @@ -489,13 +489,13 @@ def read_txt_file(filename): vz.append(vz_) if len(t) < 4: - raise ValueError('read_txt_file: File {} does not have enough statevectors'.format(filename)) + raise ValueError(f'read_txt_file: File {filename} does not have enough statevectors') return [np.array(a) for a in [t, x, y, z, vx, vy, vz]] def read_ESA_Orbit_file(filename): - ''' + """ Read orbit data from an orbit file supplied by ESA Args: @@ -508,7 +508,7 @@ def read_ESA_Orbit_file(filename): in python datetime x, y, z: Nt x 1 ndarrays - x/y/z positions of the sensor at the times t vx, vy, vz: Nt x 1 ndarrays - x/y/z velocities of the sensor at the times t - ''' + """ if ET is None: raise ImportError('read_ESA_Orbit_file: cannot import xml.etree.ElementTree') tree = ET.parse(filename) @@ -543,7 +543,7 @@ def read_ESA_Orbit_file(filename): def pick_ESA_orbit_file(list_files:list, ref_time:datetime.datetime): - """ From list of .EOF orbit files, pick the one that contains 'ref_time' """ + """From list of .EOF orbit files, pick the one that contains 'ref_time'""" orb_file = None for path in list_files: f = os.path.basename(path) @@ -568,7 +568,7 @@ def filter_ESA_orbit_file(orbit_xml: str, ESA orbit xml ref_time : datetime.datetime - Returns + Returns: ------- bool True if ref time is within orbit_xml @@ -581,10 +581,11 @@ def filter_ESA_orbit_file(orbit_xml: str, ############################ def state_to_los(svs, llh_targets): - ''' + """ Converts information from a state vector for a satellite orbit, given in terms of position and velocity, to line-of-sight information at each (lon,lat, height) coordinate requested by the user. + Args: ---------- svs - t, x, y, z, vx, vy, vz - time, position, and velocity in ECEF of the sensor @@ -593,6 +594,7 @@ def state_to_los(svs, llh_targets): Returns: ------- LOS - * x 3 matrix of LOS unit vectors in ECEF (*not* ENU) + Example: >>> import datetime >>> import numpy @@ -604,7 +606,7 @@ def state_to_los(svs, llh_targets): >>> esa_orbit_file = 'S1A_OPER_AUX_POEORB_OPOD_20181203T120749_V20181112T225942_20181114T005942.EOF' >>> svs = losr.read_ESA_Orbit_file(esa_orbit_file) >>> LOS = losr.state_to_los(*svs, [lats, lons, heights], xyz) - ''' + """ if isce is None: raise ImportError('isce3 is required for this function. Use conda to install isce3`') @@ -638,11 +640,13 @@ def cut_times(times, ref_time, pad): Slice the orbit file around the reference aquisition time. This is done by default using a three-hour window, which for Sentinel-1 empirically works out to be roughly the largest window allowed by the orbit time. + Args: ---------- times: Nt x 1 ndarray - Vector of orbit times as datetime ref_time: datetime - Reference time pad: int - integer time in seconds to use as padding + Returns: ------- idx: Nt x 1 logical ndarray - a mask of times within the padded request time. @@ -654,7 +658,7 @@ def cut_times(times, ref_time, pad): def get_radar_pos(llh, orb): - ''' + """ Calculate the coordinate of the sensor in ECEF at the time corresponding to ***. Args: @@ -667,7 +671,7 @@ def get_radar_pos(llh, orb): ------- los: ndarray - Satellite incidence angle sr: ndarray - Slant range in meters - ''' + """ if isce is None: raise ImportError('isce3 is required for this function. Use conda to install isce3`') @@ -763,7 +767,7 @@ def getTopOfAtmosphere(xyz, look_vecs, toaheight, factor=None): def get_orbit(orbit_file: Union[list, str], ref_time: datetime.datetime, pad: int): - ''' + """ Returns state vectors from an orbit file; state vectors are unique and ordered in terms of time orbit file (str | list): - user-passed file(s) containing statevectors for the sensor (can be download with sentineleof libray). Lists of files @@ -771,7 +775,7 @@ def get_orbit(orbit_file: Union[list, str], pad (int): - number of seconds to keep around the requested time (should be about 600 seconds) - ''' + """ if isce is None: raise ImportError('isce3 is required for this function. Use conda to install isce3`') diff --git a/tools/RAiDER/models/__init__.py b/tools/RAiDER/models/__init__.py index 79666992e..86162384d 100644 --- a/tools/RAiDER/models/__init__.py +++ b/tools/RAiDER/models/__init__.py @@ -6,4 +6,5 @@ from .merra2 import MERRA2 from .ncmr import NCMR + __all__ = ['HRRR', 'HRRRAK', 'GMAO', 'ERA5', 'ERA5T', 'HRES', 'MERRA2'] diff --git a/tools/RAiDER/models/credentials.py b/tools/RAiDER/models/credentials.py index 70900bcaa..9c54b76a4 100644 --- a/tools/RAiDER/models/credentials.py +++ b/tools/RAiDER/models/credentials.py @@ -1,4 +1,4 @@ -''' +""" API credential information and help url for downloading weather model data saved in a hidden file in home directory @@ -8,7 +8,7 @@ ecmwfapirc HRES email key https://api.ecmwf.int/v1 netrc GMAO, MERRA2 username password urs.earthdata.nasa.gov HRRR [public access] -''' +""" import os from pathlib import Path diff --git a/tools/RAiDER/models/customExceptions.py b/tools/RAiDER/models/customExceptions.py index 3bc9b69a9..fbc9c431b 100644 --- a/tools/RAiDER/models/customExceptions.py +++ b/tools/RAiDER/models/customExceptions.py @@ -39,7 +39,7 @@ def __init__(self): class WrongNumberOfFiles(Exception): def __init__(self, Nexp, Navail): msg = 'The number of files downloaded does not match the requested, ' - 'I expected {} and got {}, aborting'.format(Nexp, Navail) + f'I expected {Nexp} and got {Navail}, aborting' super().__init__(msg) @@ -57,10 +57,10 @@ def __init__(self, station_list=None, years=None): if (station_list is None) and (years is None): msg = 'No GNSS station data was found' elif (years is None): - msg = 'No data was found for GNSS stations {}'.format(station_list) + msg = f'No data was found for GNSS stations {station_list}' elif station_list is None: - msg = 'No data was found for years {}'.format(years) + msg = f'No data was found for years {years}' else: - msg = 'No data was found for GNSS stations {} and years {}'.format(station_list, years) + msg = f'No data was found for GNSS stations {station_list} and years {years}' super().__init__(msg) diff --git a/tools/RAiDER/models/ecmwf.py b/tools/RAiDER/models/ecmwf.py index 642cb41d4..f12afb1cc 100755 --- a/tools/RAiDER/models/ecmwf.py +++ b/tools/RAiDER/models/ecmwf.py @@ -2,25 +2,23 @@ import numpy as np import xarray as xr - from pyproj import CRS -from RAiDER.logger import logger from RAiDER import utilFcns as util +from RAiDER.logger import logger from RAiDER.models.model_levels import ( - LEVELS_137_HEIGHTS, - LEVELS_25_HEIGHTS, A_137_HRES, B_137_HRES, + LEVELS_25_HEIGHTS, + LEVELS_137_HEIGHTS, ) - -from RAiDER.models.weatherModel import WeatherModel, TIME_RES +from RAiDER.models.weatherModel import TIME_RES, WeatherModel class ECMWF(WeatherModel): - ''' + """ Implement ECMWF models - ''' + """ def __init__(self): # initialize a weather model @@ -53,12 +51,12 @@ def __model_levels__(self): def load_weather(self, f=None, *args, **kwargs): - ''' + """ Consistent class method to be implemented across all weather model types. As a result of calling this method, all of the variables (x, y, z, p, q, t, wet_refractivity, hydrostatic refractivity, e) should be fully populated. - ''' + """ f = self.files[0] if f is None else f self._load_model_level(f) @@ -123,9 +121,9 @@ def _load_model_level(self, fname): def _fetch(self, out): - ''' + """ Fetch a weather model from ECMWF - ''' + """ # bounding box plus a buffer lat_min, lat_max, lon_min, lon_max = self._ll_bounds @@ -156,7 +154,7 @@ def _get_from_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, server.retrieve({ "class": self._classname, # ERA-Interim 'dataset': self._dataset, - "expver": "{}".format(self._expver), + "expver": f"{self._expver}", # They warn me against all, but it works well "levelist": 'all', "levtype": "ml", # Model levels @@ -176,8 +174,8 @@ def _get_from_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, # be any of "3/6/9/12". "step": "0", # grid: Only regular lat/lon grids are supported. - "grid": '{}/{}'.format(lat_step, lon_step), - "area": '{}/{}/{}/{}'.format(lat_max, lon_min, lat_min, lon_max), # area: N/W/S/E + "grid": f'{lat_step}/{lon_step}', + "area": f'{lat_max}/{lon_min}/{lat_min}/{lon_max}', # area: N/W/S/E "format": "netcdf", "resol": "av", "target": out, # target: the name of the output file. @@ -193,7 +191,7 @@ def _get_from_cds( acqTime, outname ): - """ Used for ERA5 """ + """Used for ERA5""" import cdsapi c = cdsapi.Client(verify=0) @@ -216,7 +214,7 @@ def _get_from_cds( "class": "ea", "expver": "1", "levelist": 'all', - "levtype": "{}".format(self._model_level_type), # 'ml' for model levels or 'pl' for pressure levels + "levtype": f"{self._model_level_type}", # 'ml' for model levels or 'pl' for pressure levels 'param': var, "stream": "oper", "type": "an", @@ -236,7 +234,7 @@ def _get_from_cds( def _download_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step, time, out): - """ Used for HRES """ + """Used for HRES""" from ecmwfapi import ECMWFService server = ECMWFService("mars") @@ -255,18 +253,18 @@ def _download_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step { 'class': self._classname, 'dataset': self._dataset, - 'expver': "{}".format(self._expver), + 'expver': f"{self._expver}", 'resol': "av", 'stream': "oper", 'type': "an", 'levelist': "all", - 'levtype': "{}".format(self._model_level_type), + 'levtype': f"{self._model_level_type}", 'param': param, 'date': datetime.datetime.strftime(corrected_DT, "%Y-%m-%d"), 'time': "{}".format(datetime.time.strftime(corrected_DT.time(), '%H:%M')), 'step': "0", - 'grid': "{}/{}".format(lon_step, lat_step), - 'area': "{}/{}/{}/{}".format(lat_max, util.floorish(lon_min, 0.1), util.floorish(lat_min, 0.1), lon_max), + 'grid': f"{lon_step}/{lat_step}", + 'area': f"{lat_max}/{util.floorish(lon_min, 0.1)}/{util.floorish(lat_min, 0.1)}/{lon_max}", 'format': "netcdf", }, out @@ -329,10 +327,10 @@ def _load_pressure_level(self, filename, *args, **kwargs): def _makeDataCubes(self, fname, verbose=False): - ''' + """ Create a cube of data representing temperature and relative humidity at specified pressure levels - ''' + """ # get ll_bounds S, N, W, E = self._ll_bounds diff --git a/tools/RAiDER/models/era5.py b/tools/RAiDER/models/era5.py index f55d78ae7..62918d356 100755 --- a/tools/RAiDER/models/era5.py +++ b/tools/RAiDER/models/era5.py @@ -1,6 +1,6 @@ import datetime -from dateutil.relativedelta import relativedelta +from dateutil.relativedelta import relativedelta from pyproj import CRS from RAiDER.models.ecmwf import ECMWF @@ -35,9 +35,9 @@ def __init__(self): def _fetch(self, out): - ''' + """ Fetch a weather model from ECMWF - ''' + """ # bounding box plus a buffer lat_min, lat_max, lon_min, lon_max = self._ll_bounds time = self._time @@ -47,7 +47,7 @@ def _fetch(self, out): def load_weather(self, f=None, *args, **kwargs): - '''Load either pressure or model level data''' + """Load either pressure or model level data""" f = self.files[0] if f is None else f if self._model_level_type == 'pl': self._load_pressure_level(f, *args, **kwargs) diff --git a/tools/RAiDER/models/generateGACOSVRT.py b/tools/RAiDER/models/generateGACOSVRT.py index 60f703354..a449569a1 100644 --- a/tools/RAiDER/models/generateGACOSVRT.py +++ b/tools/RAiDER/models/generateGACOSVRT.py @@ -4,25 +4,25 @@ def makeVRT(filename, dtype='Float32'): - ''' + """ Use an RSC file to create a GDAL-compatible VRT file for opening GACOS weather model files - ''' + """ fields = readRSC(filename) string = vrtStr(fields['XMAX'], fields['YMAX'], fields['X_FIRST'], fields['Y_FIRST'], fields['X_STEP'], fields['Y_STEP'], filename.replace('.rsc', ''), dtype=dtype) writeStringToFile(string, filename.replace('.rsc', '').replace('.ztd', '') + '.vrt') def writeStringToFile(string, filename): - ''' + """ Write a string to a VRT file - ''' + """ with open(filename, 'w') as f: f.write(string) def readRSC(rscFilename): fields = {} - with open(rscFilename, 'r') as f: + with open(rscFilename) as f: for line in f: fieldName, value = line.strip().split() fields[fieldName] = value @@ -43,9 +43,9 @@ def vrtStr(xSize, ySize, lon1, lat1, lonStep, latStep, filename, dtype='Float32' def convertAllFiles(dirLoc): - ''' + """ convert all RSC files to VRT files contained in dirLoc - ''' + """ import glob files = glob.glob('*.rsc') for f in files: @@ -58,7 +58,7 @@ def main(): makeVRT(sys.argv[1]) elif len(sys.argv) == 3: convertAllFiles(sys.argv[1]) - print('Converting all RSC files in {}'.format(sys.argv[1])) + print(f'Converting all RSC files in {sys.argv[1]}') else: print('Usage: ') print('python3 generateGACOSVRT.py ') diff --git a/tools/RAiDER/models/gmao.py b/tools/RAiDER/models/gmao.py index 62c0574d1..e03ecab9d 100755 --- a/tools/RAiDER/models/gmao.py +++ b/tools/RAiDER/models/gmao.py @@ -1,18 +1,19 @@ -import os import datetime -import numpy as np +import os import shutil + import h5py +import numpy as np import pydap.cas.urs import pydap.client from pyproj import CRS -from RAiDER.models.weatherModel import WeatherModel, TIME_RES from RAiDER.logger import logger -from RAiDER.utilFcns import writeWeatherVarsXarray, round_date, requests_retry_session from RAiDER.models.model_levels import ( LEVELS_137_HEIGHTS, ) +from RAiDER.models.weatherModel import TIME_RES, WeatherModel +from RAiDER.utilFcns import requests_retry_session, round_date, writeWeatherVarsXarray class GMAO(WeatherModel): @@ -58,9 +59,9 @@ def __init__(self): def _fetch(self, out): - ''' + """ Fetch weather model data from GMAO - ''' + """ acqTime = self._time # calculate the array indices for slicing the GMAO variable arrays @@ -146,21 +147,20 @@ def _fetch(self, out): def load_weather(self, f=None): - ''' + """ Consistent class method to be implemented across all weather model types. As a result of calling this method, all of the variables (x, y, z, p, q, t, wet_refractivity, hydrostatic refractivity, e) should be fully populated. - ''' + """ f = self.files[0] if f is None else f self._load_model_level(f) def _load_model_level(self, filename): - ''' + """ Get the variables from the GMAO link using OpenDAP - ''' - + """ # adding the import here should become absolute when transition to netcdf from netCDF4 import Dataset with Dataset(filename, mode='r') as f: diff --git a/tools/RAiDER/models/hres.py b/tools/RAiDER/models/hres.py index 2ef341073..21dfe7108 100755 --- a/tools/RAiDER/models/hres.py +++ b/tools/RAiDER/models/hres.py @@ -1,22 +1,21 @@ import datetime import numpy as np - from pyproj import CRS from RAiDER.models.ecmwf import ECMWF -from RAiDER.models.weatherModel import WeatherModel, TIME_RES from RAiDER.models.model_levels import ( - LEVELS_91_HEIGHTS, A_91_HRES, B_91_HRES, + LEVELS_91_HEIGHTS, ) +from RAiDER.models.weatherModel import TIME_RES, WeatherModel class HRES(ECMWF): - ''' + """ Implement ECMWF models - ''' + """ def __init__(self, level_type='ml'): # initialize a weather model @@ -60,12 +59,12 @@ def update_a_b(self): self._b = B_91_HRES def load_weather(self, f=None): - ''' + """ Consistent class method to be implemented across all weather model types. As a result of calling this method, all of the variables (x, y, z, p, q, t, wet_refractivity, hydrostatic refractivity, e) should be fully populated. - ''' + """ f = self.files[0] if f is None else f if self._model_level_type == 'ml': @@ -76,9 +75,9 @@ def load_weather(self, f=None): self._load_pressure_levels(f) def _fetch(self,out): - ''' + """ Fetch a weather model from ECMWF - ''' + """ # bounding box plus a buffer lat_min, lat_max, lon_min, lon_max = self._ll_bounds time = self._time diff --git a/tools/RAiDER/models/hrrr.py b/tools/RAiDER/models/hrrr.py index 94248a6cf..71361e6f4 100644 --- a/tools/RAiDER/models/hrrr.py +++ b/tools/RAiDER/models/hrrr.py @@ -1,19 +1,19 @@ import datetime import os -import xarray +from pathlib import Path +import geopandas as gpd import numpy as np - +import xarray from herbie import Herbie -import geopandas as gpd -from pathlib import Path from pyproj import CRS, Transformer from shapely.geometry import Polygon, box -from RAiDER.utilFcns import round_date -from RAiDER.models.weatherModel import WeatherModel, TIME_RES -from RAiDER.models.model_levels import LEVELS_50_HEIGHTS from RAiDER.logger import logger +from RAiDER.models.model_levels import LEVELS_50_HEIGHTS +from RAiDER.models.weatherModel import TIME_RES, WeatherModel +from RAiDER.utilFcns import round_date + HRRR_CONUS_COVERAGE_POLYGON = Polygon(((-125, 21), (-133, 49), (-60, 49), (-72, 21))) HRRR_AK_COVERAGE_POLYGON = Polygon(((195, 40), (157, 55), (175, 70), (260, 77), (232, 52))) @@ -33,7 +33,7 @@ def check_hrrr_dataset_availability(dt: datetime) -> bool: return avail def download_hrrr_file(ll_bounds, DATE, out, model='hrrr', product='nat', fxx=0, verbose=False): - ''' + """ Download a HRRR weather model using Herbie Args: @@ -47,7 +47,7 @@ def download_hrrr_file(ll_bounds, DATE, out, model='hrrr', product='nat', fxx=0, Returns: None, writes data to a netcdf file - ''' + """ H = Herbie( DATE.strftime('%Y-%m-%d %H:%M'), model=model, @@ -94,7 +94,7 @@ def download_hrrr_file(ll_bounds, DATE, out, model='hrrr', product='nat', fxx=0, ds_out = ds_out.rename({'gh': 'z', coord: 'levels'}) # projection information - ds_out["proj"] = int() + ds_out["proj"] = 0 for k, v in CRS.from_user_input(ds_out.herbie.crs).to_cf().items(): ds_out.proj.attrs[k] = v for var in ds_out.data_vars: @@ -121,9 +121,9 @@ def download_hrrr_file(ll_bounds, DATE, out, model='hrrr', product='nat', fxx=0, def get_bounds_indices(SNWE, lats, lons): - ''' + """ Convert SNWE lat/lon bounds to index bounds - ''' + """ # Unpack the bounds and find the relevent indices S, N, W, E = SNWE @@ -161,9 +161,9 @@ def get_bounds_indices(SNWE, lats, lons): def load_weather_hrrr(filename): - ''' + """ Loads a weather model from a HRRR file - ''' + """ # read data from the netcdf file ds = xarray.open_dataset(filename, engine='netcdf4') # Pull the relevant data from the file @@ -260,9 +260,9 @@ def __pressure_levels__(self): def _fetch(self, out): - ''' + """ Fetch weather model data from HRRR - ''' + """ self._files = out corrected_DT = round_date(self._time, datetime.timedelta(hours=self._time_res)) self.checkTime(corrected_DT) @@ -277,10 +277,10 @@ def _fetch(self, out): def load_weather(self, f=None, *args, **kwargs): - ''' + """ Load a weather model into a python weatherModel object, from self.files if no filename is passed. - ''' + """ if f is None: f = self.files[0] if isinstance(self.files, list) else self.files @@ -301,7 +301,7 @@ def load_weather(self, f=None, *args, **kwargs): def checkValidBounds(self: WeatherModel, ll_bounds: np.ndarray): - ''' + """ Checks whether the given bounding box is valid for the HRRR or HRRRAK (i.e., intersects with the model domain at all) @@ -310,7 +310,7 @@ def checkValidBounds(self: WeatherModel, ll_bounds: np.ndarray): Returns: The weather model object - ''' + """ S, N, W, E = ll_bounds aoi = box(W, S, E, N) if self._valid_bounds.contains(aoi): @@ -389,7 +389,7 @@ def _fetch(self, out): corrected_DT = round_date(self._time, datetime.timedelta(hours=self._time_res)) self.checkTime(corrected_DT) if not corrected_DT == self._time: - logger.info('Rounded given datetime from {} to {}'.format(self._time, corrected_DT)) + logger.info(f'Rounded given datetime from {self._time} to {corrected_DT}') download_hrrr_file(bounds, corrected_DT, out, 'hrrrak', self._model_level_type) diff --git a/tools/RAiDER/models/merra2.py b/tools/RAiDER/models/merra2.py index 7e98a9de1..5c0d50824 100755 --- a/tools/RAiDER/models/merra2.py +++ b/tools/RAiDER/models/merra2.py @@ -1,19 +1,18 @@ +import datetime import os -import xarray -import datetime import numpy as np import pydap.cas.urs import pydap.client - +import xarray from pyproj import CRS -from RAiDER.models.weatherModel import WeatherModel from RAiDER.logger import logger -from RAiDER.utilFcns import writeWeatherVarsXarray, read_EarthData_loginInfo from RAiDER.models.model_levels import ( LEVELS_137_HEIGHTS, ) +from RAiDER.models.weatherModel import WeatherModel +from RAiDER.utilFcns import read_EarthData_loginInfo, writeWeatherVarsXarray # Path to Netrc file, can be controlled by env var @@ -68,9 +67,9 @@ def __init__(self): self._proj = CRS.from_epsg(4326) def _fetch(self, out): - ''' + """ Fetch weather model data from GMAO: note we only extract the lat/lon bounds for this weather model; fetching data is not needed here as we don't actually download any data using OpenDAP - ''' + """ time = self._time # check whether the file already exists @@ -124,23 +123,22 @@ def _fetch(self, out): except Exception as e: logger.debug(e) logger.exception("MERRA-2: Unable to save weathermodel to file") - raise RuntimeError('MERRA-2 failed with the following error: {}'.format(e)) + raise RuntimeError(f'MERRA-2 failed with the following error: {e}') def load_weather(self, f=None, *args, **kwargs): - ''' + """ Consistent class method to be implemented across all weather model types. As a result of calling this method, all of the variables (x, y, z, p, q, t, wet_refractivity, hydrostatic refractivity, e) should be fully populated. - ''' + """ f = self.files[0] if f is None else f self._load_model_level(f) def _load_model_level(self, filename): - ''' + """ Get the variables from the GMAO link using OpenDAP - ''' - + """ # adding the import here should become absolute when transition to netcdf ds = xarray.load_dataset(filename) lons = ds['longitude'].values diff --git a/tools/RAiDER/models/model_levels.py b/tools/RAiDER/models/model_levels.py index db3e122b6..c7434d808 100644 --- a/tools/RAiDER/models/model_levels.py +++ b/tools/RAiDER/models/model_levels.py @@ -1,4 +1,4 @@ -''' +""" Pre-defined model levels and a, b constants for the different weather models **NOTE**: The fixed heights used here are from ECMWF's _geometric_ altitudes @@ -7,7 +7,7 @@ the altitude to include the variation of gravity with height, while geometric altitude is the standard direct vertical distance above mean sea level (MSL)." - Wikipedia.org, https://en.wikipedia.org/wiki/International_Standard_Atmosphere -''' +""" LEVELS_137_HEIGHTS = [ 80301.65, diff --git a/tools/RAiDER/models/ncmr.py b/tools/RAiDER/models/ncmr.py index 394ab9538..b6fc7e674 100755 --- a/tools/RAiDER/models/ncmr.py +++ b/tools/RAiDER/models/ncmr.py @@ -7,25 +7,24 @@ import urllib.request import numpy as np - from pyproj import CRS -from RAiDER.models.weatherModel import WeatherModel, TIME_RES from RAiDER.logger import logger +from RAiDER.models.model_levels import ( + LEVELS_137_HEIGHTS, +) +from RAiDER.models.weatherModel import TIME_RES, WeatherModel from RAiDER.utilFcns import ( read_NCMR_loginInfo, show_progress, writeWeatherVarsXarray, ) -from RAiDER.models.model_levels import ( - LEVELS_137_HEIGHTS, -) class NCMR(WeatherModel): - ''' + """ Implement NCMRWF NCUM (named as NCMR) model in future - ''' + """ def __init__(self): # initialize a weather model @@ -64,10 +63,10 @@ def __init__(self): self._proj = CRS.from_epsg(4326) def _fetch(self, out): - ''' + """ Fetch weather model data from NCMR: note we only extract the lat/lon bounds for this weather model; fetching data is not needed here as we don't actually download data , data exist in same system - ''' + """ time = self._time # Auxillary function: @@ -77,9 +76,9 @@ def _fetch(self, out): self._files = self._download_ncmr_file(out, time, self._ll_bounds) def load_weather(self, f=None, *args, **kwargs): - ''' + """ Load NCMR model variables from existing file - ''' + """ f = self.files[0] if f is None else f # bounding box plus a buffer @@ -89,11 +88,10 @@ def load_weather(self, f=None, *args, **kwargs): self._makeDataCubes(f) def _download_ncmr_file(self, out, date_time, bounding_box): - ''' + """ Download weather model data (whole globe) from NCMR weblink, crop it to the region of interest, and save the cropped data as a standard .nc file of RAiDER (e.g. "NCMR_YYYY_MM_DD_THH_MM_SS.nc"); Temporarily download data from NCMR ftp 'https://ftp.ncmrwf.gov.in/pub/outgoing/SAC/NCUM_OSF/' and copied in weather_models folder - ''' - + """ from netCDF4 import Dataset ############# Use these lines and modify the link when actually downloading NCMR data from a weblink ############# @@ -180,9 +178,9 @@ def _download_ncmr_file(self, out, date_time, bounding_box): logger.exception("Unable to save weathermodel to file") def _makeDataCubes(self, filename): - ''' + """ Get the variables from the saved .nc file (named as "NCMR_YYYY_MM_DD_THH_MM_SS.nc") - ''' + """ from netCDF4 import Dataset # adding the import here should become absolute when transition to netcdf diff --git a/tools/RAiDER/models/plotWeather.py b/tools/RAiDER/models/plotWeather.py index 6be5cdc8d..b101f0da4 100755 --- a/tools/RAiDER/models/plotWeather.py +++ b/tools/RAiDER/models/plotWeather.py @@ -5,19 +5,22 @@ class objects. It is not designed to be used on its own apart """ import os -from RAiDER.interpolator import RegularGridInterpolator as Interpolator -from mpl_toolkits.axes_grid1 import make_axes_locatable as mal -import numpy as np -import matplotlib.pyplot as plt + import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +from mpl_toolkits.axes_grid1 import make_axes_locatable as mal + +from RAiDER.interpolator import RegularGridInterpolator as Interpolator + + mpl.use('Agg') def plot_pqt(weatherObj, savefig=True, z1=500, z2=15000): - ''' + """ Create a plot with pressure, temp, and humidity at two heights - ''' - + """ # Get the interpolator intFcn_p = Interpolator((weatherObj._xs, weatherObj._ys, weatherObj._zs), weatherObj._p.swapaxes(0, 1)) @@ -65,9 +68,9 @@ def plot_pqt(weatherObj, savefig=True, z1=500, z2=15000): plt.colorbar(im, cax=cax) sp.set_title(title) if ind == 0: - sp.set_ylabel('{} m\n'.format(z1)) + sp.set_ylabel(f'{z1} m\n') if ind == 3: - sp.set_ylabel('{} m\n'.format(z2)) + sp.set_ylabel(f'{z2} m\n') # add plots that show each variable with height zdata = weatherObj._zs[:] / 1000 @@ -95,11 +98,10 @@ def plot_pqt(weatherObj, savefig=True, z1=500, z2=15000): def plot_wh(weatherObj, savefig=True, z1=500, z2=15000): - ''' + """ Create a plot with wet refractivity and hydrostatic refractivity, at two different heights - ''' - + """ # Get the interpolator intFcn_w = Interpolator((weatherObj._xs, weatherObj._ys, weatherObj._zs), weatherObj._wet_refractivity.swapaxes(0, 1)) intFcn_h = Interpolator((weatherObj._xs, weatherObj._ys, weatherObj._zs), weatherObj._hydrostatic_refractivity.swapaxes(0, 1)) @@ -122,10 +124,10 @@ def plot_wh(weatherObj, savefig=True, z1=500, z2=15000): plots = [w1, h1, w2, h2] # titles - titles = ('Wet refractivity {}'.format(z1), - 'Hydrostatic refractivity {}'.format(z1), - '{}'.format(z2), - '{}'.format(z2)) + titles = (f'Wet refractivity {z1}', + f'Hydrostatic refractivity {z1}', + f'{z2}', + f'{z2}') # setup the plot f = plt.figure(figsize=(14, 10)) @@ -141,9 +143,9 @@ def plot_wh(weatherObj, savefig=True, z1=500, z2=15000): plt.colorbar(im, cax=cax) sp.set_title(title) if ind == 0: - sp.set_ylabel('{} m\n'.format(z1)) + sp.set_ylabel(f'{z1} m\n') if ind == 2: - sp.set_ylabel('{} m\n'.format(z2)) + sp.set_ylabel(f'{z2} m\n') if savefig: wd = os.path.dirname(os.path.dirname(weatherObj._out_name)) diff --git a/tools/RAiDER/models/template.py b/tools/RAiDER/models/template.py index a753605ba..76c2fc513 100644 --- a/tools/RAiDER/models/template.py +++ b/tools/RAiDER/models/template.py @@ -1,10 +1,12 @@ -from pyproj import CRS -import numpy as np import datetime -from RAiDER.models.weatherModel import WeatherModel + +import numpy as np +from pyproj import CRS + from RAiDER.models.model_levels import ( LEVELS_137_HEIGHTS, ) +from RAiDER.models.weatherModel import WeatherModel class customModelReader(WeatherModel): @@ -54,11 +56,11 @@ def __init__(self): x0 = 0 y0 = 0 earth_radius = 6371229 - p1 = CRS('+proj=lcc +lat_1={lat1} +lat_2={lat2} +lat_0={lat0} +lon_0={lon0} +x_0={x0} +y_0={y0} +a={a} +b={a} +units=m +no_defs'.format(lat1=lat1, lat2=lat2, lat0=lat0, lon0=lon0, x0=x0, y0=y0, a=earth_radius)) + p1 = CRS(f'+proj=lcc +lat_1={lat1} +lat_2={lat2} +lat_0={lat0} +lon_0={lon0} +x_0={x0} +y_0={y0} +a={earth_radius} +b={earth_radius} +units=m +no_defs') self._proj = p1 def _fetch(self, out): - ''' + """ Fetch weather model data from the custom weather model "ABCD" Inputs (no need to change in the custom weather model reader): lats - latitude @@ -66,7 +68,7 @@ def _fetch(self, out): time - datatime object (year,month,day,hour,minute,second) out - name of downloaded dataset file from the custom weather model server Nextra - buffer of latitude/longitude for determining the bounding box - ''' + """ # Auxilliary function: # download dataset of the custom weather model "ABCD" from a server and then save it to a file named out. # This function needs to be writen by the users. For download from the weather model server, the weather model @@ -76,12 +78,11 @@ def _fetch(self, out): self._files = self._download_abcd_file(out, 'abcd', self._time, self._ll_bounds) def load_weather(self, filename): - ''' + """ Load weather model variables from the downloaded file named filename Inputs: filename - filename of the downloaded weather model file - ''' - + """ # Auxilliary function: # read individual variables (in 3-D cube format with exactly the same dimension) from downloaded file # This function needs to be writen by the users. For downloaded file from the weather model server, @@ -132,7 +133,7 @@ def load_weather(self, filename): ########### def _download_abcd_file(self, out, model_name, date_time, bounding_box): - ''' + """ Auxilliary function: Download weather model data from a server Inputs: @@ -142,11 +143,11 @@ def _download_abcd_file(self, out, model_name, date_time, bounding_box): bounding_box - lat/lon bounding box for the region of interest Output: out - returned filename from input - ''' + """ pass def _makeDataCubes(self, filename): - ''' + """ Auxilliary function: Read 3-D data cubes from downloaded file or directly from weather model weblink (in which case, there is no need to download and save any file; rather, the weblink needs to be hardcoded in the custom reader, e.g. GMAO) @@ -161,5 +162,5 @@ def _makeDataCubes(self, filename): q - humidity (3-D data cube; could be relative humidity or specific humidity) p - pressure level (3-D data cube; could be pressure level (preferred) or surface pressure) hgt - height (3-D data cube; could be geopotential height or topographic height (preferred)) - ''' + """ pass diff --git a/tools/RAiDER/models/weatherModel.py b/tools/RAiDER/models/weatherModel.py index 4b08ee6d9..ee595733c 100755 --- a/tools/RAiDER/models/weatherModel.py +++ b/tools/RAiDER/models/weatherModel.py @@ -4,24 +4,21 @@ import numpy as np import xarray - from pyproj import CRS -from shapely.geometry import box from shapely.affinity import translate +from shapely.geometry import box from shapely.ops import unary_union -from RAiDER.constants import _ZREF, _ZMIN, _g0 from RAiDER import utilFcns as util +from RAiDER.constants import _ZMIN, _ZREF, _g0 from RAiDER.interpolate import interpolate_along_axis from RAiDER.interpolator import fillna3D from RAiDER.logger import logger -from RAiDER.models import plotWeather as plots, weatherModel -from RAiDER.models.customExceptions import ( - DatetimeOutsideRange -) -from RAiDER.utilFcns import ( - robmax, robmin, calcgeoh, transform_coords, clip_bbox -) +from RAiDER.models import plotWeather as plots +from RAiDER.models import weatherModel +from RAiDER.models.customExceptions import DatetimeOutsideRange +from RAiDER.utilFcns import calcgeoh, clip_bbox, robmax, robmin, transform_coords + TIME_RES = {'GMAO': 3, 'ECMWF': 1, @@ -33,9 +30,9 @@ } class WeatherModel(ABC): - ''' + """ Implement a generic weather model for getting estimated SAR delays - ''' + """ def __init__(self): # Initialize model-specific constants/parameters @@ -103,32 +100,32 @@ def __init__(self): def __str__(self): string = '\n' string += '======Weather Model class object=====\n' - string += 'Weather model time: {}\n'.format(self._time) - string += 'Latitude resolution: {}\n'.format(self._lat_res) - string += 'Longitude resolution: {}\n'.format(self._lon_res) - string += 'Native projection: {}\n'.format(self._proj) - string += 'ZMIN: {}\n'.format(self._zmin) - string += 'ZMAX: {}\n'.format(self._zmax) - string += 'k1 = {}\n'.format(self._k1) - string += 'k2 = {}\n'.format(self._k2) - string += 'k3 = {}\n'.format(self._k3) - string += 'Humidity type = {}\n'.format(self._humidityType) + string += f'Weather model time: {self._time}\n' + string += f'Latitude resolution: {self._lat_res}\n' + string += f'Longitude resolution: {self._lon_res}\n' + string += f'Native projection: {self._proj}\n' + string += f'ZMIN: {self._zmin}\n' + string += f'ZMAX: {self._zmax}\n' + string += f'k1 = {self._k1}\n' + string += f'k2 = {self._k2}\n' + string += f'k3 = {self._k3}\n' + string += f'Humidity type = {self._humidityType}\n' string += '=====================================\n' - string += 'Class name: {}\n'.format(self._classname) - string += 'Dataset: {}\n'.format(self._dataset) + string += f'Class name: {self._classname}\n' + string += f'Dataset: {self._dataset}\n' string += '=====================================\n' - string += 'A: {}\n'.format(self._a) - string += 'B: {}\n'.format(self._b) + string += f'A: {self._a}\n' + string += f'B: {self._b}\n' if self._p is not None: string += 'Number of points in Lon/Lat = {}/{}\n'.format(*self._p.shape[:2]) - string += 'Total number of grid points (3D): {}\n'.format(np.prod(self._p.shape)) + string += f'Total number of grid points (3D): {np.prod(self._p.shape)}\n' if self._xs.size == 0: - string += 'Minimum/Maximum y: {: 4.2f}/{: 4.2f}\n'\ - .format(robmin(self._ys), robmax(self._ys)) - string += 'Minimum/Maximum x: {: 4.2f}/{: 4.2f}\n'\ - .format(robmin(self._xs), robmax(self._xs)) - string += 'Minimum/Maximum zs/heights: {: 10.2f}/{: 10.2f}\n'\ - .format(robmin(self._zs), robmax(self._zs)) + string += f'Minimum/Maximum y: {robmin(self._ys): 4.2f}/{robmax(self._ys): 4.2f}\n'\ + + string += f'Minimum/Maximum x: {robmin(self._xs): 4.2f}/{robmax(self._xs): 4.2f}\n'\ + + string += f'Minimum/Maximum zs/heights: {robmin(self._zs): 10.2f}/{robmax(self._zs): 10.2f}\n'\ + string += '=====================================\n' return str(string) @@ -146,7 +143,7 @@ def getLLRes(self): def fetch(self, out, time): - ''' + """ Checks the input datetime against the valid date range for the model and then calls the model _fetch routine @@ -155,7 +152,7 @@ def fetch(self, out, time): out - ll_bounds - 4 x 1 array, SNWE time = UTC datetime - ''' + """ self.checkTime(time) self.setTime(time) @@ -169,9 +166,9 @@ def fetch(self, out, time): @abstractmethod def _fetch(self, out): - ''' + """ Placeholder method. Should be implemented in each weather model type class - ''' + """ pass @@ -180,7 +177,7 @@ def getTime(self): def setTime(self, time, fmt='%Y-%m-%dT%H:%M:%S'): - ''' Set the time for a weather model ''' + """Set the time for a weather model""" if isinstance(time, str): self._time = datetime.datetime.strptime(time, fmt) elif isinstance(time, datetime.datetime): @@ -196,14 +193,14 @@ def get_latlon_bounds(self): def set_latlon_bounds(self, ll_bounds, Nextra=2, output_spacing=None): - ''' + """ Need to correct lat/lon bounds because not all of the weather models have valid data exactly bounded by -90/90 (lats) and -180/180 (lons); for GMAO and MERRA2, need to adjust the longitude higher end with an extra buffer; for other models, the exact bounds are close to -90/90 (lats) and -180/180 (lons) and thus can be rounded to the above regions (either in the downloading-file API or subsetting- data API) without problems. - ''' + """ ex_buffer_lon_max = 0.0 if self._Name in 'HRRR HRRR-AK HRES'.split(): @@ -229,7 +226,7 @@ def set_latlon_bounds(self, ll_bounds, Nextra=2, output_spacing=None): def get_wmLoc(self): - """ Get the path to the direct with the weather model files """ + """Get the path to the direct with the weather model files""" if self._wmLoc is None: wmLoc = os.path.join(os.getcwd(), 'weather_files') else: @@ -238,7 +235,7 @@ def get_wmLoc(self): def set_wmLoc(self, weather_model_directory:str): - """ Set the path to the directory with the weather model files """ + """Set the path to the directory with the weather model files""" self._wmLoc = weather_model_directory @@ -248,10 +245,10 @@ def load( _zlevels=None, **kwargs ): - ''' + """ Calls the load_weather method. Each model class should define a load_weather method appropriate for that class. 'args' should be one or more filenames. - ''' + """ # If the weather file has already been processed, do nothing outLoc = self.get_wmLoc() path_wm_raw = make_raw_weather_data_filename(outLoc, self.Model(), self.getTime()) @@ -278,40 +275,40 @@ def load( @abstractmethod def load_weather(self, *args, **kwargs): - ''' + """ Placeholder method. Should be implemented in each weather model type class - ''' + """ pass def plot(self, plotType='pqt', savefig=True): - ''' + """ Plotting method. Valid plot types are 'pqt' - ''' + """ if plotType == 'pqt': plot = plots.plot_pqt(self, savefig) elif plotType == 'wh': plot = plots.plot_wh(self, savefig) else: - raise RuntimeError('WeatherModel.plot: No plotType named {}'.format(plotType)) + raise RuntimeError(f'WeatherModel.plot: No plotType named {plotType}') return plot def checkTime(self, time): - ''' + """ Checks the time against the lag time and valid date range for the given model type Parameters: time - Python datetime object - Raises: + Raises: Different errors depending on the issue - ''' + """ start_time = self._valid_range[0] end_time = self._valid_range[1] if not isinstance(time, datetime.datetime): - raise ValueError('"time" should be a Python datetime object, instead it is {}'.format(time)) + raise ValueError(f'"time" should be a Python datetime object, instead it is {time}') # This is needed because Python now gets angry if you try to compare non-timezone-aware # objects with time-zone aware objects. @@ -334,7 +331,7 @@ def checkTime(self, time): def setLevelType(self, levelType): - '''Set the level type to model levels or pressure levels''' + """Set the level type to model levels or pressure levels""" if levelType in 'ml pl nat prs'.split(): self._model_level_type = levelType else: @@ -347,16 +344,16 @@ def setLevelType(self, levelType): def _convertmb2Pa(self, pres): - ''' + """ Convert pressure in millibars to Pascals - ''' + """ return 100 * pres def _get_heights(self, lats, geo_hgt, geo_ht_fill=np.nan): - ''' + """ Transform geo heights to WGS84 ellipsoidal heights - ''' + """ geo_ht_fix = np.where(geo_hgt != geo_ht_fill, geo_hgt, np.nan) lats_full = np.broadcast_to(lats[...,np.newaxis], geo_ht_fix.shape) self._zs = util.geo_to_ht(lats_full, geo_ht_fix) @@ -389,16 +386,16 @@ def _find_e_from_rh(self): def _get_wet_refractivity(self): - ''' + """ Calculate the wet delay from pressure, temperature, and e - ''' + """ self._wet_refractivity = self._k2 * self._e / self._t + self._k3 * self._e / self._t**2 def _get_hydro_refractivity(self): - ''' + """ Calculate the hydrostatic delay from pressure and temperature - ''' + """ self._hydrostatic_refractivity = self._k1 * self._p / self._t @@ -411,13 +408,12 @@ def getHydroRefractivity(self): def _adjust_grid(self, ll_bounds=None): - ''' + """ This function pads the weather grid with a level at self._zmin, if it does not already go that low. <> <> - ''' - + """ if self._zmin < np.nanmin(self._zs): # first add in a new layer at zmin self._zs = np.insert(self._zs, 0, self._zmin) @@ -432,10 +428,10 @@ def _adjust_grid(self, ll_bounds=None): def _getZTD(self): - ''' + """ Compute the full slant tropospheric delay for each weather model grid node, using the reference height zref - ''' + """ wet = self.getWetRefractivity() hydro = self.getHydroRefractivity() @@ -453,9 +449,9 @@ def _getZTD(self): def _getExtent(self, lats, lons): - ''' + """ get the bounding box around a set of lats/lons - ''' + """ if (lats.size == 1) & (lons.size == 1): return [lats - self._lat_res, lats + self._lat_res, lons - self._lon_res, lons + self._lon_res] elif (lats.size > 1) & (lons.size > 1): @@ -478,12 +474,11 @@ def bbox(self) -> list: list xmin, ymin, xmax, ymax - Raises + Raises: ------ ValueError When `self.files` is None. """ - if self._bbox is None: path_weather_model = self.out_file(self.get_wmLoc()) if not os.path.exists(path_weather_model): @@ -514,7 +509,7 @@ def checkValidBounds( self: weatherModel, ll_bounds: np.ndarray, ): - ''' + """ Checks whether the given bounding box is valid for the model (i.e., intersects with the model domain at all) @@ -523,7 +518,7 @@ def checkValidBounds( Returns: The weather model object - ''' + """ S, N, W, E = ll_bounds if box(W, S, E, N).intersects(self._valid_bounds): Mod = self @@ -592,10 +587,10 @@ def checkContainment(self: weatherModel, def _isOutside(self, extent1, extent2): - ''' + """ Determine whether any of extent1 lies outside extent2 extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon] - ''' + """ t1 = extent1[0] < extent2[0] t2 = extent1[1] > extent2[1] t3 = extent1[2] < extent2[2] @@ -606,9 +601,9 @@ def _isOutside(self, extent1, extent2): def _trimExtent(self, extent): - ''' + """ get the bounding box around a set of lats/lons - ''' + """ lat = self._lats.copy() lon = self._lons.copy() lat[np.isnan(lat)] = np.nanmean(lat) @@ -642,7 +637,7 @@ def _trimExtent(self, extent): def _calculategeoh(self, z, lnsp): - ''' + """ Function to calculate pressure, geopotential, and geopotential height from the surface pressure and model levels provided by a weather model. The model levels are numbered from the highest eleveation to the lowest. @@ -655,14 +650,14 @@ def _calculategeoh(self, z, lnsp): pressurelvs - The pressure at each of the model levels for each of the input points geoheight - The geopotential heights - ''' + """ return calcgeoh(lnsp, self._t, self._q, z, self._a, self._b, self._R_d, self._levels) def getProjection(self): - ''' + """ Returns: the native weather projection, which should be a pyproj object - ''' + """ return self._proj @@ -671,9 +666,9 @@ def getPoints(self): def _uniform_in_z(self, _zlevels=None): - ''' + """ Interpolate all variables to a regular grid in z - ''' + """ nx, ny = self._p.shape[:2] # new regular z-spacing @@ -702,9 +697,9 @@ def _uniform_in_z(self, _zlevels=None): def _checkForNans(self): - ''' + """ Fill in NaN-values - ''' + """ self._p = fillna3D(self._p) self._t = fillna3D(self._t, fill_value=1e16) # to avoid division by zero later on self._e = fillna3D(self._e) @@ -720,9 +715,9 @@ def out_file(self, outLoc): def filename(self, time=None, outLoc='weather_files'): - ''' + """ Create a filename to store the weather model - ''' + """ os.makedirs(outLoc, exist_ok=True) if time is None: @@ -742,11 +737,11 @@ def filename(self, time=None, outLoc='weather_files'): def write(self): - ''' + """ By calling the abstract/modular netcdf writer (RAiDER.utilFcns.write2NETCDF4core), write the weather model data and refractivity to an NETCDF4 file that can be accessed by external programs. - ''' + """ # Generate the filename f = self._out_name @@ -799,7 +794,7 @@ def write(self): ds['hydro_total'].attrs['standard_name'] = 'total_hydrostatic_refractivity' # projection information - ds["proj"] = int() + ds["proj"] = 0 for k, v in self._proj.to_cf().items(): ds.proj.attrs[k] = v for var in ds.data_vars: @@ -826,7 +821,7 @@ def make_weather_model_filename(name, time, ll_bounds): def make_raw_weather_data_filename(outLoc, name, time): - ''' Filename generator for the raw downloaded weather model data ''' + """Filename generator for the raw downloaded weather model data""" f = os.path.join( outLoc, '{}_{}.{}'.format( @@ -874,7 +869,7 @@ def find_svp(t): def get_mapping(proj): - '''Get CF-complient projection information from a proj''' + """Get CF-complient projection information from a proj""" # In case of WGS-84 lat/lon, keep it simple if proj.to_epsg()==4326: return 'WGS84' diff --git a/tools/RAiDER/models/wrf.py b/tools/RAiDER/models/wrf.py index 827495a54..5d3070e5e 100644 --- a/tools/RAiDER/models/wrf.py +++ b/tools/RAiDER/models/wrf.py @@ -2,7 +2,7 @@ import scipy.io.netcdf as netcdf from pyproj import CRS, Transformer -from RAiDER.models.weatherModel import WeatherModel, TIME_RES +from RAiDER.models.weatherModel import TIME_RES, WeatherModel # Need to incorporate this snippet into this part of the code. @@ -15,9 +15,9 @@ # lats, lons = wrf.wm_nodes(*weather_files) # class WRF(WeatherModel): - ''' + """ WRF class definition, based on the WeatherModel base class. - ''' + """ # TODO: finish implementing def __init__(self): @@ -37,9 +37,9 @@ def _fetch(self): pass def load_weather(self, file1, file2, *args, **kwargs): - ''' + """ Consistent class method to be implemented across all weather model types - ''' + """ try: lons, lats = self._get_wm_nodes(file1) self._read_netcdf(file2) @@ -162,29 +162,29 @@ def _read_netcdf(self, weatherFile, defNul=None): class UnitTypeError(Exception): - ''' + """ Define a unit type exception for easily formatting error messages for units - ''' + """ def __init___(self, varName, unittype): - msg = "Unknown units for {}: '{}'".format(varName, unittype) + msg = f"Unknown units for {varName}: '{unittype}'" Exception.__init__(self, msg) def checkUnits(unitCheck, varName): - ''' + """ Implement a check that the units are as expected - ''' + """ unitDict = {'pressure': 'Pa', 'temperature': 'K', 'relative humidity': '%', 'geopotential': 'm'} if unitCheck != unitDict[varName]: raise UnitTypeError(varName, unitCheck) def getNullValue(var): - ''' + """ Get the null (or fill) value if it exists, otherwise set the null value to defNullValue - ''' + """ # NetCDF files have the ability to record their nodata value, but in the # particular NetCDF files that I'm reading, this field is left # unspecified and a nodata value of -999 is used. The solution I'm using diff --git a/tools/RAiDER/processWM.py b/tools/RAiDER/processWM.py index 6bc71b4e1..f3524f640 100755 --- a/tools/RAiDER/processWM.py +++ b/tools/RAiDER/processWM.py @@ -11,19 +11,23 @@ import numpy as np from RAiDER.logger import logger -from RAiDER.models.weatherModel import make_raw_weather_data_filename, checkContainment_raw, make_weather_model_filename from RAiDER.models.customExceptions import ( - ExistingWeatherModelTooSmall, DatetimeOutsideRange, TryToKeepGoingError, CriticalError + CriticalError, + DatetimeOutsideRange, + ExistingWeatherModelTooSmall, + TryToKeepGoingError, ) +from RAiDER.models.weatherModel import checkContainment_raw, make_raw_weather_data_filename, make_weather_model_filename + def prepareWeatherModel( - weather_model, - time, - ll_bounds, - download_only: bool=False, - makePlots: bool=False, - force_download: bool=False, - ) -> str: + weather_model, + time, + ll_bounds, + download_only: bool=False, + makePlots: bool=False, + force_download: bool=False, +) -> str: """Parse inputs to download and prepare a weather model grid for interpolation Args: @@ -142,20 +146,19 @@ def prepareWeatherModel( def _weather_model_debug( - los, - lats, - lons, - ll_bounds, - weather_model, - wmLoc, - time, - out, - download_only - ): + los, + lats, + lons, + ll_bounds, + weather_model, + wmLoc, + time, + out, + download_only +): """ raiderWeatherModelDebug main function. """ - logger.debug('Starting to run the weather model calculation with debugging plots') logger.debug('Time type: %s', type(time)) logger.debug('Time: %s', time.strftime('%Y%m%d')) diff --git a/tools/RAiDER/s1_azimuth_timing.py b/tools/RAiDER/s1_azimuth_timing.py index dae72b19b..4ba60af2f 100644 --- a/tools/RAiDER/s1_azimuth_timing.py +++ b/tools/RAiDER/s1_azimuth_timing.py @@ -6,6 +6,7 @@ import pandas as pd from shapely.geometry import Point + try: import isce3.ext.isce3 as isce except ImportError: @@ -28,7 +29,7 @@ def _asf_query(point: Point, end : datetime.datetime buffer_degrees : float, optional - Returns + Returns: ------- list[str] """ @@ -58,7 +59,7 @@ def get_slc_id_from_point_and_time(lon: float, buffer_seconds : int, optional Do not recommend adjusting this, by default 600, to ensure enough padding for multiple orbit files - Returns + Returns: ------- list All slc_ids returned by asf_search @@ -81,13 +82,13 @@ def get_azimuth_time_grid(lon_mesh: np.ndarray, lat_mesh: np.ndarray, hgt_mesh: np.ndarray, orb: 'isce.core.Orbit') -> np.ndarray: - ''' + """ Source: https://github.com/dbekaert/RAiDER/blob/dev/tools/RAiDER/losreader.py#L601C1-L674C22 lon_mesh, lat_mesh, hgt_mesh are coordinate arrays (this routine makes a mesh to comute azimuth timing grid) Technically, this is "sensor neutral" since it uses an orb object. - ''' + """ if isce is None: raise ImportError('isce3 is required for this function. Use conda to install isce3`') @@ -148,7 +149,7 @@ def get_s1_azimuth_time_grid(lon: np.ndarray, 1 dimensional coordinate array or 3d mesh of coordinates dt : datetime.datetime - Returns + Returns: ------- np.ndarray Cube whose coordinates are hgt x lat x lon with each pixel @@ -220,7 +221,7 @@ def get_n_closest_datetimes(ref_time: datetime.datetime, nearest 0, 2, 4, etc. times. Must be divisible by 24 otherwise is not consistent across all days. - Returns + Returns: ------- list[datetime.datetime] List of closest dates ordered by absolute proximity. If two dates have same distance to ref_time, @@ -288,7 +289,7 @@ def get_times_for_azimuth_interpolation(ref_time: datetime.datetime, buffer_in_seconds : int, optional Buffer for filtering absolute times, by default 300 (or 5 minutes) - Returns + Returns: ------- list[datetime.datetime] 2 or 3 closest times within 1 time step (plust the buffer) and the reference time @@ -330,7 +331,7 @@ def get_inverse_weights_for_dates(azimuth_time_array: np.ndarray, No check of equi-spaced dates are done so not specifying temporal window hours requires dates to be derived from valid model time steps - Returns + Returns: ------- list[np.ndarray] Weighting per pixel with respect to each date diff --git a/tools/RAiDER/s1_orbits.py b/tools/RAiDER/s1_orbits.py index c7cff6462..cb506593e 100644 --- a/tools/RAiDER/s1_orbits.py +++ b/tools/RAiDER/s1_orbits.py @@ -7,6 +7,7 @@ import eof.download from hyp3lib import get_orb + from RAiDER.logger import logger @@ -83,7 +84,6 @@ def get_orbits_from_slc_ids_hyp3lib( slc_ids: list, orbit_directory: str = None ) -> dict: """Reference: https://github.com/ACCESS-Cloud-Based-InSAR/DockerizedTopsApp/blob/dev/isce2_topsapp/localize_orbits.py#L23""" - # Populates env variables to netrc as required for sentineleof _ = ensure_orbit_credentials() esa_username, _, esa_password = netrc.netrc().authenticators(ESA_CDSE_HOST) diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index a2c5fcbee..12033a086 100755 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -1,13 +1,13 @@ """Geodesy-related utility functions.""" import os import re -import xarray - from datetime import datetime, timedelta, timezone -from numpy import ndarray -from pyproj import Transformer, CRS, Proj import numpy as np +import xarray +from numpy import ndarray +from pyproj import CRS, Proj, Transformer + # Optional imports try: @@ -42,14 +42,14 @@ def projectDelays(delay, inc): - '''Project zenith delays to LOS''' + """Project zenith delays to LOS""" if inc==90: raise ZeroDivisionError return delay / cosd(inc) def floorish(val, frac): - '''Round a value to the lower fractional part''' + """Round a value to the lower fractional part""" return val - (val % frac) @@ -108,7 +108,7 @@ def enu2ecef( def ecef2enu(xyz, lat, lon, height): - '''Convert ECEF xyz to ENU''' + """Convert ECEF xyz to ENU""" x, y, z = xyz[..., 0], xyz[..., 1], xyz[..., 2] t = cosd(lon) * x + sind(lon) * y @@ -120,9 +120,9 @@ def ecef2enu(xyz, lat, lon, height): def rio_profile(fname): - ''' + """ Reads the profile of a rasterio file - ''' + """ if rasterio is None: raise ImportError('RAiDER.utilFcns: rio_profile - rasterio is not installed') @@ -142,7 +142,7 @@ def rio_profile(fname): def rio_extents(profile): - """ Get a bounding box in SNWE from a rasterio profile """ + """Get a bounding box in SNWE from a rasterio profile""" gt = profile["transform"].to_gdal() xSize = profile["width"] ySize = profile["height"] @@ -152,9 +152,9 @@ def rio_extents(profile): def rio_open(fname, returnProj=False, userNDV=None, band=None): - ''' + """ Reads a rasterio-compatible raster file and returns the data and profile - ''' + """ if rasterio is None: raise ImportError('RAiDER.utilFcns: rio_open - rasterio is not installed') @@ -207,7 +207,7 @@ def nodataToNan(inarr, listofvals): def rio_stats(fname, band=1): - ''' + """ Read a rasterio-compatible file and pull the metadata. Args: @@ -218,7 +218,7 @@ def rio_stats(fname, band=1): stats - a list of stats for the specified band proj - CRS/projection information for the file gt - geotransform for the data - ''' + """ if rasterio is None: raise ImportError('RAiDER.utilFcns: rio_stats - rasterio is not installed') @@ -255,12 +255,12 @@ def get_file_and_band(filestr): ) def writeArrayToRaster(array, filename, noDataValue=0., fmt='ENVI', proj=None, gt=None): - ''' + """ write a numpy array to a GDAL-readable raster - ''' + """ array_shp = np.shape(array) if array.ndim != 2: - raise RuntimeError('writeArrayToRaster: cannot write an array of shape {} to a raster image'.format(array_shp)) + raise RuntimeError(f'writeArrayToRaster: cannot write an array of shape {array_shp} to a raster image') # Data type if "complex" in str(array.dtype): @@ -336,28 +336,28 @@ def _least_nonzero(a): def robmin(a): - ''' + """ Get the minimum of an array, accounting for empty lists - ''' + """ return np.nanmin(a) def robmax(a): - ''' + """ Get the minimum of an array, accounting for empty lists - ''' + """ return np.nanmax(a) def _get_g_ll(lats): - ''' + """ Compute the variation in gravity constant with latitude - ''' + """ return G1 * (1 - 0.002637 * cosd(2 * lats) + 0.0000059 * (cosd(2 * lats))**2) def get_Re(lats): - ''' + """ Returns earth radius as a function of latitude for WGS84 Args: @@ -374,7 +374,7 @@ def get_Re(lats): array([6378137., 6372770.5219805, 6367417.56705189, 6362078.07851428, 6356752.]) >>> assert output[0] == 6378137 # (Rmax) >>> assert output[-1] == 6356752 # (Rmin) - ''' + """ return np.sqrt(1 / (((cosd(lats)**2) / Rmax**2) + ((sind(lats)**2) / Rmin**2))) @@ -411,20 +411,20 @@ def geo_to_ht(lats, hts): def padLower(invar): - ''' + """ add a layer of data below the lowest current z-level at height zmin - ''' + """ new_var = _least_nonzero(invar) return np.concatenate((new_var[:, :, np.newaxis], invar), axis=2) def round_time(dt, roundTo=60): - ''' + """ Round a datetime object to any time lapse in seconds dt: datetime.datetime object roundTo: Closest number of seconds to round to, default 1 minute. Source: https://stackoverflow.com/questions/3463930/how-to-round-the-minute-of-a-datetime-object/10854034#10854034 - ''' + """ seconds = (dt.replace(tzinfo=None) - dt.min).seconds rounding = (seconds + roundTo / 2) // roundTo * roundTo return dt + timedelta(0, rounding - seconds, -dt.microsecond) @@ -433,7 +433,7 @@ def round_time(dt, roundTo=60): def writeDelays(aoi, wetDelay, hydroDelay, wetFilename, hydroFilename=None, outformat=None, ndv=0.): - """ Write the delay numpy arrays to files in the format specified """ + """Write the delay numpy arrays to files in the format specified""" if pd is None: raise ImportError('pandas is required to write GNSS delays to a file') @@ -473,9 +473,9 @@ def writeDelays(aoi, wetDelay, hydroDelay, def getTimeFromFile(filename): - ''' + """ Parse a filename to get a date-time - ''' + """ fmt = '%Y_%m_%d_T%H_%M_%S' p = re.compile(r'\d{4}_\d{2}_\d{2}_T\d{2}_\d{2}_\d{2}') out = p.search(filename).group() @@ -623,7 +623,7 @@ def clip_bbox(bbox, spacing): def requests_retry_session(retries=10, session=None): - """ https://www.peterbe.com/plog/best-practice-with-retries-with-requests """ + """https://www.peterbe.com/plog/best-practice-with-retries-with-requests""" import requests from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry @@ -676,7 +676,7 @@ def writeWeatherVarsXarray(lat, lon, h, q, p, t, dt, crs, outName=None, NoDataVa ds['q'].attrs['units'] = 'kg kg-1' ds['t'].attrs['units'] = 'K' - ds["proj"] = int() + ds["proj"] = 0 for k, v in crs.to_cf().items(): ds.proj.attrs[k] = v for var in ds.data_vars: @@ -687,7 +687,7 @@ def writeWeatherVarsXarray(lat, lon, h, q, p, t, dt, crs, outName=None, NoDataVa def convertLons(inLons): - '''Convert lons from 0-360 to -180-180''' + """Convert lons from 0-360 to -180-180""" mask = inLons > 180 outLons = inLons outLons[mask] = outLons[mask] - 360 @@ -701,7 +701,7 @@ def read_NCMR_loginInfo(filepath=None): if filepath is None: filepath = str(Path.home()) + '/.ncmrlogin' - f = open(filepath, 'r') + f = open(filepath) lines = f.readlines() url = lines[0].strip().split(': ')[1] username = lines[1].strip().split(': ')[1] @@ -719,7 +719,7 @@ def read_EarthData_loginInfo(filepath=None): def show_progress(block_num, block_size, total_size): - '''Show download progress''' + """Show download progress""" if progressbar is None: raise ImportError('RAiDER.utilFcns: show_progress - progressbar is not available') @@ -737,7 +737,7 @@ def show_progress(block_num, block_size, total_size): def getChunkSize(in_shape): - '''Create a reasonable chunk size''' + """Create a reasonable chunk size""" if mp is None: raise ImportError('RAiDER.utilFcns: getChunkSize - multiprocessing is not available') minChunkSize = 100 @@ -753,10 +753,11 @@ def getChunkSize(in_shape): def calcgeoh(lnsp, t, q, z, a, b, R_d, num_levels): - ''' + """ Calculate pressure, geopotential, and geopotential height from the surface pressure and model levels provided by a weather model. The model levels are numbered from the highest eleveation to the lowest. + Args: ---------- lnsp: ndarray - [y, x] array of log surface pressure @@ -766,13 +767,14 @@ def calcgeoh(lnsp, t, q, z, a, b, R_d, num_levels): a: ndarray - [z] vector of a values b: ndarray - [z] vector of b values num_levels: int - integer number of model levels + Returns: ------- geopotential - The geopotential in units of height times acceleration pressurelvs - The pressure at each of the model levels for each of the input points geoheight - The geopotential heights - ''' + """ geopotential = np.zeros_like(t) pressurelvs = np.zeros_like(geopotential) geoheight = np.zeros_like(geopotential) @@ -783,8 +785,8 @@ def calcgeoh(lnsp, t, q, z, a, b, R_d, num_levels): if len(a) != num_levels + 1 or len(b) != num_levels + 1: raise ValueError( - 'I have here a model with {} levels, but parameters a '.format(num_levels) + - 'and b have lengths {} and {} respectively. Of '.format(len(a), len(b)) + + f'I have here a model with {num_levels} levels, but parameters a ' + + f'and b have lengths {len(a)} and {len(b)} respectively. Of ' + 'course, these three numbers should be equal.') # Integrate up into the atmosphere from *lowest level* @@ -879,7 +881,7 @@ def get_nearest_wmtimes(t0, time_delta): def get_dt(t1,t2): - ''' + """ Helper function for getting the absolute difference in seconds between two python datetimes @@ -894,7 +896,7 @@ def get_dt(t1,t2): >>> from RAiDER.utilFcns import get_dt >>> get_dt(datetime.datetime(2020,1,1,5,0,0), datetime.datetime(2020,1,1,0,0,0)) 18000.0 - ''' + """ return np.abs((t1 - t2).total_seconds()) From f6322a4e5a1022f865a716d2fb774e2bb2aa6e8a Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:34:28 -0500 Subject: [PATCH 08/76] More automatic fixes - Ensure every docstring description has punctuation - Fixed some typos - Put some comments into imperative mood; will do this for all of them later - Add type annotations where ruff can infer them --- tools/RAiDER/aria/calcGUNW.py | 17 +-- tools/RAiDER/aria/prepFromGUNW.py | 38 ++++--- tools/RAiDER/checkArgs.py | 9 +- tools/RAiDER/cli/__main__.py | 2 +- tools/RAiDER/cli/conf.py | 2 +- tools/RAiDER/cli/parser.py | 8 +- tools/RAiDER/cli/raider.py | 18 ++-- tools/RAiDER/cli/statsPlot.py | 112 ++++++------------- tools/RAiDER/cli/validators.py | 76 +++++-------- tools/RAiDER/delay.py | 17 +-- tools/RAiDER/delayFcns.py | 6 +- tools/RAiDER/getStationDelays.py | 18 +--- tools/RAiDER/gnss/downloadGNSSDelays.py | 38 +++---- tools/RAiDER/gnss/processDelayFiles.py | 26 ++--- tools/RAiDER/interpolator.py | 8 +- tools/RAiDER/llreader.py | 57 +++++----- tools/RAiDER/logger.py | 6 +- tools/RAiDER/losreader.py | 60 +++++------ tools/RAiDER/models/credentials.py | 4 +- tools/RAiDER/models/customExceptions.py | 18 ++-- tools/RAiDER/models/ecmwf.py | 30 +++--- tools/RAiDER/models/era5.py | 12 +-- tools/RAiDER/models/era5t.py | 2 +- tools/RAiDER/models/erai.py | 2 +- tools/RAiDER/models/generateGACOSVRT.py | 18 ++-- tools/RAiDER/models/gmao.py | 16 ++- tools/RAiDER/models/hres.py | 16 ++- tools/RAiDER/models/hrrr.py | 32 +++--- tools/RAiDER/models/merra2.py | 16 ++- tools/RAiDER/models/model_levels.py | 2 +- tools/RAiDER/models/ncmr.py | 26 ++--- tools/RAiDER/models/plotWeather.py | 6 +- tools/RAiDER/models/template.py | 18 ++-- tools/RAiDER/models/weatherModel.py | 136 +++++++++--------------- tools/RAiDER/models/wrf.py | 36 ++----- tools/RAiDER/processWM.py | 8 +- tools/RAiDER/s1_azimuth_timing.py | 9 +- tools/RAiDER/s1_orbits.py | 8 +- tools/RAiDER/utilFcns.py | 82 ++++++-------- 39 files changed, 396 insertions(+), 619 deletions(-) diff --git a/tools/RAiDER/aria/calcGUNW.py b/tools/RAiDER/aria/calcGUNW.py index b54f802c7..65e2a43ee 100644 --- a/tools/RAiDER/aria/calcGUNW.py +++ b/tools/RAiDER/aria/calcGUNW.py @@ -1,6 +1,6 @@ """ -Calculate the interferometric phase from the 4 delays files of a GUNW -Write it to disk +Calculate the interferometric phase from the 4 delays files of a GUNW and +write it to disk. """ import os from datetime import datetime @@ -23,7 +23,8 @@ def compute_delays_slc(cube_filenames: list, wavelength: float) -> xr.Dataset: - """Get delays from standard RAiDER output formatting ouput including radian + """ + Get delays from standard RAiDER output formatting ouput including radian conversion and metadata. Parameters @@ -107,9 +108,9 @@ def compute_delays_slc(cube_filenames: list, wavelength: float) -> xr.Dataset: return ds_slc.rename(z=DIM_NAMES[0], y=DIM_NAMES[1], x=DIM_NAMES[2]) -def update_gunw_slc(path_gunw:str, ds_slc): - """Update the path_gunw file using the slc delays in ds_slc""" ## first need to delete the variable; only can seem to with h5 +def update_gunw_slc(path_gunw:str, ds_slc) -> None: + """Update the path_gunw file using the slc delays in ds_slc.""" with h5py.File(path_gunw, 'a') as h5: for k in TROPO_GROUP.split(): h5 = h5[k] @@ -177,8 +178,8 @@ def update_gunw_slc(path_gunw:str, ds_slc): return -def update_gunw_version(path_gunw): - """Temporary hack for updating version to test aria-tools""" +def update_gunw_version(path_gunw) -> None: + """Temporary hack for updating version to test aria-tools.""" with Dataset(path_gunw, mode='a') as ds: ds.version = '1c' return @@ -188,7 +189,7 @@ def tropo_gunw_slc(cube_filenames: list, path_gunw: str, wavelength: float) -> xr.Dataset: """ - Computes and formats the troposphere phase delay for GUNW from RAiDER outputs. + Compute and format the troposphere phase delay for GUNW from RAiDER outputs. Parameters ---------- diff --git a/tools/RAiDER/aria/prepFromGUNW.py b/tools/RAiDER/aria/prepFromGUNW.py index b926538e0..336d3dcea 100644 --- a/tools/RAiDER/aria/prepFromGUNW.py +++ b/tools/RAiDER/aria/prepFromGUNW.py @@ -9,6 +9,7 @@ import sys from dataclasses import dataclass from datetime import datetime, timedelta, timezone +from typing import Literal import numpy as np import pandas as pd @@ -49,8 +50,10 @@ def _get_acq_time_from_gunw_id(gunw_id: str, reference_or_secondary: str) -> dat def check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id: str) -> bool: - """Determines if all the times for azimuth interpolation are available using Herbie; note that not all 1 hour times - are available within the said date range of HRRR. + """ + Determine if all the times for azimuth interpolation are available using + Herbie. Note that not all 1 hour times are available within the said date + range of HRRR. Parameters ---------- @@ -78,7 +81,7 @@ def check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id: st def get_slc_ids_from_gunw(gunw_path: str, reference_or_secondary: str = 'reference') -> list[str]: - #Example input: test/gunw_test_data/S1-GUNW-D-R-059-tops-20230320_20220418-180300-00179W_00051N-PP-c92e-v2_0_6.nc + # Example input: test/gunw_test_data/S1-GUNW-D-R-059-tops-20230320_20220418-180300-00179W_00051N-PP-c92e-v2_0_6.nc if reference_or_secondary not in ['reference', 'secondary']: raise ValueError('"reference_or_secondary" must be either "reference" or "secondary"') group = f'science/radarMetaData/inputSLC/{reference_or_secondary}' @@ -96,8 +99,9 @@ def get_acq_time_from_slc_id(slc_id: str) -> pd.Timestamp: def check_weather_model_availability(gunw_path: str, weather_model_name: str) -> bool: - """Checks weather reference and secondary dates of GUNW occur within - weather model valid range + """ + Check weather reference and secondary dates of GUNW occur within + weather model valid range. Parameters ---------- @@ -179,7 +183,7 @@ def __post_init__(self): def get_bbox(self): - """Get the bounding box (SNWE) from an ARIA GUNW product""" + """Get the bounding box (SNWE) from an ARIA GUNW product.""" with xr.open_dataset(self.path_gunw) as ds: poly_str = ds['productBoundingBox'].data[0].decode('utf-8') @@ -189,15 +193,15 @@ def get_bbox(self): return [S, N, W, E] - def make_fname(self): - """Match the ref/sec filename (SLC dates may be different around edge cases)""" + def make_fname(self) -> str: + """Match the ref/sec filename (SLC dates may be different around edge cases).""" ref, sec = os.path.basename(self.path_gunw).split('-')[6].split('_') mid_time = os.path.basename(self.path_gunw).split('-')[7] return f'{ref}-{sec}_{mid_time}' def get_datetimes(self): - """Get the datetimes and set the satellite for orbit""" + """Get the datetimes and set the satellite for orbit.""" ref_sec = self.get_slc_dt() middates = [] for aq in ref_sec: @@ -209,7 +213,7 @@ def get_datetimes(self): def get_slc_dt(self): - """Grab the SLC start date and time from the GUNW""" + """Grab the SLC start date and time from the GUNW.""" group = 'science/radarMetaData/inputSLC' lst_sten = [] for i, key in enumerate('reference secondary'.split()): @@ -248,7 +252,7 @@ def get_slc_dt(self): return lst_sten - def get_look_dir(self): + def get_look_dir(self) -> Literal['right', 'left']: look_dir = os.path.basename(self.path_gunw).split('-')[3].lower() return 'right' if look_dir == 'r' else 'left' @@ -261,7 +265,7 @@ def get_wavelength(self): def get_orbit_file(self): - """Get orbit file for reference (GUNW: first & later date)""" + """Get orbit file for reference (GUNW: first & later date).""" orbit_dir = os.path.join(self.out_dir, 'orbits') os.makedirs(orbit_dir, exist_ok=True) @@ -288,7 +292,7 @@ def get_version(self): def getHeights(self): - """Get the 4 height levels within a GUNW""" + """Get the 4 height levels within a GUNW.""" group ='science/grids/imagingGeometry' with xr.open_dataset(self.path_gunw, group=group) as ds: hgts = ds.heightsMeta.data.tolist() @@ -296,7 +300,7 @@ def getHeights(self): def calc_spacing_UTM(self, posting:float=0.01): - """Convert desired horizontal posting in degrees to meters + """Convert desired horizontal posting in degrees to meters. Want to calculate delays close to native model resolution (3 km for HRR) """ @@ -316,7 +320,7 @@ def calc_spacing_UTM(self, posting:float=0.01): def makeLatLonGrid_native(self): - """Make LatLonGrid at GUNW spacing (90m = 0.00083333º)""" + """Make LatLonGrid at GUNW spacing (90m = 0.00083333º).""" group = 'science/grids/data' with xr.open_dataset(self.path_gunw, group=group) as ds0: lats = ds0.latitude.data @@ -340,7 +344,7 @@ def makeLatLonGrid_native(self): def make_cube(self): - """Make LatLonGrid at GUNW spacing (90m = 0.00083333º)""" + """Make LatLonGrid at GUNW spacing (90m = 0.00083333º).""" group = 'science/grids/data' with xr.open_dataset(self.path_gunw, group=group) as ds0: lats0 = ds0.latitude.data @@ -391,7 +395,7 @@ def update_yaml(dct_cfg:dict, dst:str='GUNW.yaml'): def main(args): - """Read parameters needed for RAiDER from ARIA Standard Products (GUNW)""" + """Read parameters needed for RAiDER from ARIA Standard Products (GUNW).""" # Check if WEATHER MODEL API credentials hidden file exists, if not create it or raise ERROR credentials.check_api(args.weather_model, args.api_uid, args.api_key) diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index 1c605b2be..0c35b729f 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -17,8 +17,7 @@ def checkArgs(args): """ - Helper fcn for checking argument compatibility and returns the - correct variables + Check argument compatibility and return the correct variables. """ ######################################################################################################################### # Directories @@ -34,9 +33,9 @@ def checkArgs(args): args.date_list = [datetime.combine(d, args.time) for d in args.date_list] if (len(args.date_list) > 1) & (args.orbit_file is not None): logger.warning('Only one orbit file is being used to get the ' - 'look vectors for all requested times, if you ' - 'want to use separate orbit files you will ' - 'need to run raider separately for each time.') + 'look vectors for all requested times, if you ' + 'want to use separate orbit files you will ' + 'need to run raider separately for each time.') args.los.setTime(args.date_list[0]) diff --git a/tools/RAiDER/cli/__main__.py b/tools/RAiDER/cli/__main__.py index a6bf1e73d..a716faf07 100644 --- a/tools/RAiDER/cli/__main__.py +++ b/tools/RAiDER/cli/__main__.py @@ -5,7 +5,7 @@ import RAiDER.cli.conf as conf -def main(): +def main() -> None: parser = argparse.ArgumentParser( prefix_chars='+', formatter_class=argparse.ArgumentDefaultsHelpFormatter diff --git a/tools/RAiDER/cli/conf.py b/tools/RAiDER/cli/conf.py index 8d1faaa03..53a70c9ca 100644 --- a/tools/RAiDER/cli/conf.py +++ b/tools/RAiDER/cli/conf.py @@ -1,7 +1,7 @@ LOGGER_PATH = None -def setLoggerPath(path): +def setLoggerPath(path) -> None: global LOGGER_PATH LOGGER_PATH = path diff --git a/tools/RAiDER/cli/parser.py b/tools/RAiDER/cli/parser.py index 53ffb7e0b..5e6a97b20 100644 --- a/tools/RAiDER/cli/parser.py +++ b/tools/RAiDER/cli/parser.py @@ -4,7 +4,7 @@ from RAiDER.cli.validators import BBoxAction, IntegerMappingType -def add_cpus(parser: argparse.ArgumentParser): +def add_cpus(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--cpus', help='The number of cpus to be used for multiprocessing or "all" for ' @@ -14,7 +14,7 @@ def add_cpus(parser: argparse.ArgumentParser): ) -def add_verbose(parser: argparse.ArgumentParser): +def add_verbose(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--verbose', '-v', help='Run in verbose mode', @@ -23,7 +23,7 @@ def add_verbose(parser: argparse.ArgumentParser): ) -def add_out(parser: argparse.ArgumentParser): +def add_out(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--out', help='Output directory', @@ -31,7 +31,7 @@ def add_out(parser: argparse.ArgumentParser): ) -def add_bbox(parser: argparse.ArgumentParser): +def add_bbox(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--bbox', '-b', help="Bounding box", diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 8f6aaf08a..ef81e087d 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -379,7 +379,7 @@ def calcDelays(iargs=None): # ------------------------------------------------------ downloadGNSSDelays.py -def downloadGNSS(): +def downloadGNSS() -> None: """Parse command line arguments using argparse.""" from RAiDER.gnss.downloadGNSSDelays import main as dlGNSS p = argparse.ArgumentParser( @@ -623,10 +623,8 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: # ------------------------------------------------------------ processDelays.py -def combineZTDFiles(): - """ - Command-line program to process delay files from RAiDER and GNSS into a single file. - """ +def combineZTDFiles() -> None: + """Command-line program to process delay files from RAiDER and GNSS into a single file.""" from RAiDER.gnss.processDelayFiles import combineDelayFiles, create_parser, main p = create_parser() @@ -652,7 +650,7 @@ def combineZTDFiles(): def getWeatherFile(wfiles, times, t, model, interp_method='none'): """ - # Time interpolation + # Time interpolation. # # Need to handle various cases, including if the exact weather model time is # requested, or if one or more datetimes are not available from the weather @@ -724,7 +722,7 @@ def getWeatherFile(wfiles, times, t, model, interp_method='none'): def combine_weather_files(wfiles, t, model, interp_method='center_time'): - """Interpolate downloaded weather files and save to a single file""" + """Interpolate downloaded weather files and save to a single file.""" STYLE = {'center_time': '_timeInterp_', 'azimuth_time_grid': '_timeInterpAziGrid_'} @@ -769,7 +767,7 @@ def combine_weather_files(wfiles, t, model, interp_method='center_time'): def combine_files_using_azimuth_time(wfiles, t, times): - """Combine files using azimuth time interpolation""" + """Combine files using azimuth time interpolation.""" # read the individual datetime datasets datasets = [xr.open_dataset(f) for f in wfiles] @@ -806,7 +804,7 @@ def combine_files_using_azimuth_time(wfiles, t, times): def get_weights_time_interp(times, t): - """Calculate weights for time interpolation using simple inverse linear weighting""" + """Calculate weights for time interpolation using simple inverse linear weighting.""" date1, date2 = times wgts = [1 - get_dt(t, date1) / get_dt(date2, date1), 1 - get_dt(date2, t) / get_dt(date2, date1)] @@ -822,7 +820,7 @@ def get_weights_time_interp(times, t): def get_time_grid_for_aztime_interp(datasets, t, model): - """Calculate the time-varying grid for use with azimuth time interpolation""" + """Calculate the time-varying grid for use with azimuth time interpolation.""" # Each model will require some inspection here # the subsequent s1 azimuth time grid requires dimension # inputs to all have same dimensions and either be diff --git a/tools/RAiDER/cli/statsPlot.py b/tools/RAiDER/cli/statsPlot.py index bfcf856b0..ec8fb96de 100755 --- a/tools/RAiDER/cli/statsPlot.py +++ b/tools/RAiDER/cli/statsPlot.py @@ -172,9 +172,7 @@ def cmd_line_parse(iargs=None): def convert_SI(val, unit_in, unit_out): - """ - Convert input to desired units - """ + """Convert input to desired units.""" SI = {'mm': 0.001, 'cm': 0.01, 'm': 1.0, 'km': 1000., 'mm^2': 1e-6, 'cm^2': 1e-4, 'm^2': 1.0, 'km^2': 1e+6} @@ -195,9 +193,7 @@ def convert_SI(val, unit_in, unit_out): def midpoint(p1, p2): - """ - Calculate central longitude for '--time_lines' option - """ + """Calculate central longitude for '--time_lines' option.""" import math if p1[1] == p2[1]: @@ -215,9 +211,7 @@ def midpoint(p1, p2): def save_gridfile(df, gridfile_type, fname, plotbbox, spacing, unit, colorbarfmt='%.2f', stationsongrids=False, time_lines=False, dtype="float32", noData=np.nan): - """ - Function to save gridded-arrays as GDAL-readable file. - """ + """Function to save gridded-arrays as GDAL-readable file.""" # Pass metadata metadata_dict = {} metadata_dict['gridfile_type'] = gridfile_type @@ -251,9 +245,7 @@ def save_gridfile(df, gridfile_type, fname, plotbbox, spacing, unit, def load_gridfile(fname, unit): - """ - Function to load gridded-arrays saved from previous runs. - """ + """Function to load gridded-arrays saved from previous runs.""" try: with rasterio.open(fname) as src: grid_array = src.read(1).astype(float) @@ -307,11 +299,9 @@ def load_gridfile(fname, unit): class VariogramAnalysis: - """ - Class which ingests dataframe output from 'RaiderStats' class and performs variogram analysis. - """ + """Class which ingests dataframe output from 'RaiderStats' class and performs variogram analysis.""" - def __init__(self, filearg, gridpoints, col_name, unit='m', workdir='./', seasonalinterval=None, densitythreshold=10, binnedvariogram=False, numCPUs=8, variogram_per_timeslice=False, variogram_errlimit='inf'): + def __init__(self, filearg, gridpoints, col_name, unit='m', workdir='./', seasonalinterval=None, densitythreshold=10, binnedvariogram=False, numCPUs=8, variogram_per_timeslice=False, variogram_errlimit='inf') -> None: self.df = filearg self.col_name = col_name self.unit = unit @@ -325,9 +315,7 @@ def __init__(self, filearg, gridpoints, col_name, unit='m', workdir='./', season self.variogram_errlimit = float(variogram_errlimit) def _get_samples(self, data, Nsamp=1000): - """ - pull samples from a 2D image for variogram analysis - """ + """Pull samples from a 2D image for variogram analysis.""" import random if len(data) < self.densitythreshold: logger.warning('Less than {} points for this gridcell', self.densitythreshold) @@ -347,32 +335,23 @@ def _get_samples(self, data, Nsamp=1000): return d, indpars def _get_XY(self, x2d, y2d, indpars): - """ - Given a list of indices, return the x,y locations - from two matrices - """ + """Given a list of indices, return the x,y locations from two matrices.""" x = np.array([[x2d[r[0]], x2d[r[1]]] for r in indpars]) y = np.array([[y2d[r[0]], y2d[r[1]]] for r in indpars]) return x, y def _get_distances(self, XY): - """ - Return the distances between each point in a list of points - """ + """Return the distances between each point in a list of points.""" from scipy.spatial.distance import cdist return np.diag(cdist(XY[:, :, 0], XY[:, :, 1], metric='euclidean')) def _get_variogram(self, XY, xy=None): - """ - Return variograms - """ + """Return variograms.""" return 0.5 * np.square(XY - xy) # XY = 1st col xy= 2nd col def _emp_vario(self, x, y, data, Nsamp=1000): - """ - Compute empirical semivariance - """ + """Compute empirical semivariance.""" # remove NaNs if possible mask = ~np.isnan(data) if False in mask: @@ -395,9 +374,7 @@ def _emp_vario(self, x, y, data, Nsamp=1000): return dists, vario def _binned_vario(self, hEff, rawVario, xBin=None): - """ - return a binned empirical variogram - """ + """Return a binned empirical variogram.""" if xBin is None: with warnings.catch_warnings(): warnings.filterwarnings("ignore", message="All-NaN slice encountered") @@ -425,9 +402,7 @@ def _binned_vario(self, hEff, rawVario, xBin=None): return np.array(hExp), np.array(expVario) def _fit_vario(self, dists, vario, model=None, x0=None, Nparm=None, ub=None): - """ - Fit a variogram model to data - """ + """Fit a variogram model to data.""" from scipy.optimize import least_squares def resid(x, d, v, m): @@ -465,12 +440,9 @@ def resid(x, d, v, m): return res_robust, d_test, v_test - # this would be expontential plus nugget + # this would be exponential plus nugget def __exponential__(self, parms, h, nugget=False): - """ - returns a variogram model given a set of arguments and - key-word arguments - """ + """Return variogram model given a set of arguments and keyword arguments.""" # a = range, b = sill, c = nugget model a, b, c = parms with warnings.catch_warnings(): @@ -482,16 +454,12 @@ def __exponential__(self, parms, h, nugget=False): # this would be gaussian plus nugget def __gaussian__(self, parms, h): - """ - returns a Gaussian variogram model - """ + """Returns a Gaussian variogram model.""" a, b, c = parms return b * (1 - np.exp(-np.square(h) / (a**2))) + c def _append_variogram(self, grid_ind, grid_subset): - """ - For a given grid-cell, iterate through time slices to generate/append empirical variogram(s) - """ + """For a given grid-cell, iterate through time slices to generate/append empirical variogram(s).""" # Comprehensive arrays recording data across all time epochs for given station dists_arr = [] vario_arr = [] @@ -583,9 +551,7 @@ def _append_variogram(self, grid_ind, grid_subset): return self.TOT_good_slices, self.TOT_res_robust_arr, self.TOT_res_robust_rmse, self.gridcenterlist def create_variograms(self): - """ - Iterate through grid-cells and time slices to generate empirical variogram(s) - """ + """Iterate through grid-cells and time slices to generate empirical variogram(s).""" # track data for plotting self.TOT_good_slices = [] self.TOT_res_robust_arr = [] @@ -625,10 +591,8 @@ def create_variograms(self): return TOT_grids, self.TOT_res_robust_arr, self.TOT_res_robust_rmse - def plot_variogram(self, gridID, timeslice, coords, workdir='./', d_test=None, v_test=None, res_robust=None, dists=None, vario=None, dists_binned=None, vario_binned=None, seasonalinterval=None): - """ - Make empirical and/or experimental variogram fit plots - """ + def plot_variogram(self, gridID, timeslice, coords, workdir='./', d_test=None, v_test=None, res_robust=None, dists=None, vario=None, dists_binned=None, vario_binned=None, seasonalinterval=None) -> None: + """Make empirical and/or experimental variogram fit plots.""" # If specified workdir doesn't exist, create it if not os.path.exists(workdir): os.mkdir(workdir) @@ -680,9 +644,7 @@ def plot_variogram(self, gridID, timeslice, coords, workdir='./', d_test=None, v class RaiderStats: - """ - Class which loads standard weather model/GPS delay files and generates a series of user-requested statistics and graphics. - """ + """Class which loads standard weather model/GPS delay files and generates a series of user-requested statistics and graphics.""" # import dependencies import glob @@ -692,7 +654,7 @@ def __init__(self, filearg, col_name, unit='m', workdir='./', bbox=None, spacing usr_colormap='hot_r', grid_heatmap=False, grid_delay_mean=False, grid_delay_median=False, grid_delay_stdev=False, grid_seasonal_phase=False, grid_delay_absolute_mean=False, grid_delay_absolute_median=False, grid_delay_absolute_stdev=False, grid_seasonal_absolute_phase=False, grid_to_raster=False, min_span=[2, 0.6], - period_limit=0., numCPUs=8, phaseamp_per_station=False): + period_limit=0., numCPUs=8, phaseamp_per_station=False) -> None: self.fname = filearg self.col_name = col_name self.unit = unit @@ -831,7 +793,7 @@ def __init__(self, filearg, col_name, unit='m', workdir='./', bbox=None, spacing self.create_DF() def _get_extent(self): # dataset, spacing=1, userbbox=None - """Get the bbox, spacing in deg (by default 1deg), optionally pass user-specified bbox. Output array in WESN degrees""" + """Get the bbox, spacing in deg (by default 1deg), optionally pass user-specified bbox. Output array in WESN degrees.""" extent = [np.floor(min(self.df['Lon'])), np.ceil(max(self.df['Lon'])), np.floor(min(self.df['Lat'])), np.ceil(max(self.df['Lat']))] if self.bbox is not None: @@ -890,7 +852,7 @@ def _get_extent(self): # dataset, spacing=1, userbbox=None def _check_stationgrid_intersection(self, stat_ID): """ Return index of grid cell which intersects with station - Note: Fast, but assumes station locations don't change + Note: Fast, but assumes station locations don't change. """ coord = Point((self.unique_points[1][self.unique_points[0].index( stat_ID)], self.unique_points[2][self.unique_points[0].index(stat_ID)])) @@ -903,9 +865,7 @@ def _check_stationgrid_intersection(self, stat_ID): return 'NaN' def _reader(self): - """ - Read a input file - """ + """Read a input file.""" try: data = pd.read_csv(self.fname, parse_dates=['Datetime']) data['Date'] = data['Datetime'].apply(lambda x: x.date()) @@ -937,10 +897,8 @@ def _reader(self): return data - def create_DF(self): - """ - Create dataframe. - """ + def create_DF(self) -> None: + """Create dataframe.""" # Open file self.df = self._reader() @@ -1395,7 +1353,7 @@ def _amplitude_and_phase(self, station, tt, yy, min_span=2, min_frac=0.6, period "amp", "omega", "phase", "offset", "freq", "period" and "fitfunc". Minimum time span in years (min_span), minimum fractional observations in span (min_frac), and period limit (period_limit) enforced for statistical analysis. - Source: https://stackoverflow.com/questions/16716302/how-do-i-fit-a-sine-curve-to-my-data-with-pylab-and-numpy + Source: https://stackoverflow.com/questions/16716302/how-do-i-fit-a-sine-curve-to-my-data-with-pylab-and-numpy. """ ampfit = {} phsfit = {} @@ -1530,15 +1488,11 @@ def fitfunc(t): self.phsfit_c, self.periodfit_c, self.seasonalfit_rmse def _sine_function_base(self, t, A, w, p, c): - """ - Base function for modeling sinusoidal amplitude/phase fits. - """ + """Base function for modeling sinusoidal amplitude/phase fits.""" return A * np.sin(w * t + p) + c def __call__(self, gridarr, plottype, workdir='./', drawgridlines=False, colorbarfmt='%.2f', stationsongrids=None, resValue=5, plotFormat='pdf', userTitle=None): - """ - Visualize a suite of statistics w.r.t. stations. Pass either a list of points or a gridded array as the first argument. Alternatively, you may superimpose your gridded array with a supplementary list of points by passing the latter through the stationsongrids argument. - """ + """Visualize a suite of statistics w.r.t. stations. Pass either a list of points or a gridded array as the first argument. Alternatively, you may superimpose your gridded array with a supplementary list of points by passing the latter through the stationsongrids argument.""" from cartopy import crs as ccrs from cartopy import feature as cfeature from cartopy.mpl.ticker import LatitudeFormatter, LongitudeFormatter @@ -1790,10 +1744,10 @@ def stats_analyses( binnedvariogram, variogram_per_timeslice, variogram_errlimit -): +) -> None: """ Main workflow for generating a suite of plots to illustrate spatiotemporal distribution - and/or character of zenith delays + and/or character of zenith delays. """ if verbose: logger.setLevel(logging.DEBUG) @@ -2052,7 +2006,7 @@ def stats_analyses( df_stats(df_stats.grid_variogram_rmse, 'grid_variogram_rmse', workdir=os.path.join(workdir, 'figures'), drawgridlines=drawgridlines, colorbarfmt='%.2e', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) -def main(): +def main() -> None: inps = cmd_line_parse() stats_analyses( diff --git a/tools/RAiDER/cli/validators.py b/tools/RAiDER/cli/validators.py index 35ece1e8e..e64081978 100755 --- a/tools/RAiDER/cli/validators.py +++ b/tools/RAiDER/cli/validators.py @@ -63,9 +63,7 @@ def get_los(args): def get_heights(args, out, station_file, bounding_box=None): - """ - Parse the Height info and download a DEM if needed - """ + """Parse the Height info and download a DEM if needed.""" dem_path = out out = { @@ -124,9 +122,7 @@ def get_heights(args, out, station_file, bounding_box=None): def get_query_region(args): - """ - Parse the query region from inputs - """ + """Parse the query region from inputs.""" # Get bounds from the inputs # make sure this is first if args.get('use_dem_latlon'): @@ -168,9 +164,7 @@ def get_query_region(args): def enforce_bbox(bbox): - """ - Enforce a valid bounding box - """ + """Enforce a valid bounding box.""" if isinstance(bbox, str): bbox = [float(d) for d in bbox.strip().split()] else: @@ -196,9 +190,7 @@ def enforce_bbox(bbox): def parse_dates(arg_dict): - """ - Determine the requested dates from the input parameters - """ + """Determine the requested dates from the input parameters.""" if arg_dict.get('date_list'): l = arg_dict['date_list'] if isinstance(l, str): @@ -231,9 +223,7 @@ def parse_dates(arg_dict): def enforce_valid_dates(arg): - """ - Parse a date from a string in pseudo-ISO 8601 format. - """ + """Parse a date from a string in pseudo-ISO 8601 format.""" year_formats = ( '%Y-%m-%d', '%Y%m%d', @@ -254,9 +244,7 @@ def enforce_valid_dates(arg): def enforce_time(arg_dict): - """ - Parse an input time (required to be ISO 8601) - """ + """Parse an input time (required to be ISO 8601).""" try: arg_dict['time'] = convert_time(arg_dict['time']) except KeyError: @@ -305,7 +293,9 @@ def convert_time(inp): def modelName2Module(model_name): - """Turn an arbitrary string into a module name. + """ + Turn an arbitrary string into a module name. + Takes as input a model name, which hopefully looks like ERA-I, and converts it to a module name, which will look like erai. I doesn't always produce a valid module name, but that's not the goal. The @@ -314,7 +304,7 @@ def modelName2Module(model_name): model_name - Name of an allowed weather model (e.g., 'era-5') Outputs: module_name - Name of the module - wmObject - callable, weather model object + wmObject - callable, weather model object. """ module_name = 'RAiDER.models.' + model_name.lower().replace('-', '') model_module = importlib.import_module(module_name) @@ -323,9 +313,7 @@ def modelName2Module(model_name): def getBufferedExtent(lats, lons=None, buf=0.): - """ - get the bounding box around a set of lats/lons - """ + """Get the bounding box around a set of lats/lons.""" if lons is None: lats, lons = lats[..., 0], lons[..., 1] @@ -347,41 +335,35 @@ def getBufferedExtent(lats, lons=None, buf=0.): return np.array(out) -def isOutside(extent1, extent2): +def isOutside(extent1, extent2) -> bool: """ - Determine whether any of extent1 lies outside extent2 - extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon] - Equal extents are considered "inside" + Determine whether any of extent1 lies outside extent2. + extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon]. + Equal extents are considered "inside". """ t1 = extent1[0] < extent2[0] t2 = extent1[1] > extent2[1] t3 = extent1[2] < extent2[2] t4 = extent1[3] > extent2[3] - if np.any([t1, t2, t3, t4]): - return True - return False + return np.any([t1, t2, t3, t4]) -def isInside(extent1, extent2): +def isInside(extent1, extent2) -> bool: """ - Determine whether all of extent1 lies inside extent2 + Determine whether all of extent1 lies inside extent2. extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon]. - Equal extents are considered "inside" + Equal extents are considered "inside". """ t1 = extent1[0] <= extent2[0] t2 = extent1[1] >= extent2[1] t3 = extent1[2] <= extent2[2] t4 = extent1[3] >= extent2[3] - if np.all([t1, t2, t3, t4]): - return True - return False + return np.all([t1, t2, t3, t4]) ## below are for downloadGNSSDelays def date_type(arg): - """ - Parse a date from a string in pseudo-ISO 8601 format. - """ + """Parse a date from a string in pseudo-ISO 8601 format.""" year_formats = ( '%Y-%m-%d', '%Y%m%d', @@ -414,12 +396,12 @@ class MappingType: """ UNSET = object() - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: self.mapping = kwargs self._default = self.UNSET def default(self, default): - """Set a default value if no mapping is found""" + """Set a default value if no mapping is found.""" self._default = default return self @@ -448,7 +430,7 @@ class IntegerType: ``` """ - def __init__(self, lo=None, hi=None): + def __init__(self, lo=None, hi=None) -> None: self.lo = lo self.hi = hi @@ -476,7 +458,7 @@ class IntegerMappingType(MappingType, IntegerType): ``` """ - def __init__(self, lo=None, hi=None, mapping={}, **kwargs): + def __init__(self, lo=None, hi=None, mapping={}, **kwargs) -> None: IntegerType.__init__(self, lo, hi) kwargs.update(mapping) MappingType.__init__(self, **kwargs) @@ -489,7 +471,7 @@ def __call__(self, arg): class DateListAction(Action): - """An Action that parses and stores a list of dates""" + """An Action that parses and stores a list of dates.""" def __init__( self, @@ -503,7 +485,7 @@ def __init__( required=False, help=None, metavar=None - ): + ) -> None: if type is not date_type: raise ValueError("type must be `date_type`!") @@ -543,7 +525,7 @@ def __call__(self, parser, namespace, values, option_string=None): class BBoxAction(Action): - """An Action that parses and stores a valid bounding box""" + """An Action that parses and stores a valid bounding box.""" def __init__( self, @@ -557,7 +539,7 @@ def __init__( required=False, help=None, metavar=None - ): + ) -> None: if nargs != 4: raise ValueError("nargs must be 4!") diff --git a/tools/RAiDER/delay.py b/tools/RAiDER/delay.py index 9c311f512..f1eda9d90 100755 --- a/tools/RAiDER/delay.py +++ b/tools/RAiDER/delay.py @@ -124,9 +124,7 @@ def tropo_delay( def _get_delays_on_cube(dt, weather_model_file, wm_proj, aoi, heights, los, crs, zref, nproc=1): - """ - raider cube generation function. - """ + """Raider cube generation function.""" zpts = np.array(heights) try: @@ -194,9 +192,7 @@ def _get_delays_on_cube(dt, weather_model_file, wm_proj, aoi, heights, los, crs, def _build_cube(xpts, ypts, zpts, model_crs, pts_crs, interpolators): - """ - Iterate over interpolators and build a cube using Zenith - """ + """Iterate over interpolators and build a cube using Zenith.""" # Create a regular 2D grid xx, yy = np.meshgrid(xpts, ypts) @@ -228,7 +224,7 @@ def _build_cube_ray( MAX_TROPO_HEIGHT=_ZREF, ): """ - Iterate over interpolators and build a cube using raytracing + Iterate over interpolators and build a cube using raytracing. MAX_TROPO_HEIGHT should not extend above the top of the weather model """ @@ -332,9 +328,7 @@ def _build_cube_ray( def writeResultsToXarray(dt, xpts, ypts, zpts, crs, wetDelay, hydroDelay, weather_model_file, out_type): - """ - write a 1-D array to a NETCDF5 file - """ + """Write a 1-D array to a NETCDF5 file.""" # Modify this as needed for NISAR / other projects ds = xarray.Dataset( data_vars=dict( @@ -406,8 +400,7 @@ def writeResultsToXarray(dt, xpts, ypts, zpts, crs, wetDelay, hydroDelay, weathe def transformPoints(lats: np.ndarray, lons: np.ndarray, hgts: np.ndarray, old_proj: CRS, new_proj: CRS) -> np.ndarray: """ - Transform lat/lon/hgt data to an array of points in a new - projection + Transform lat/lon/hgt data to an array of points in a new projection. Args: lats: ndarray - WGS-84 latitude (EPSG: 4326) diff --git a/tools/RAiDER/delayFcns.py b/tools/RAiDER/delayFcns.py index 99e68c0d4..8b97806e5 100755 --- a/tools/RAiDER/delayFcns.py +++ b/tools/RAiDER/delayFcns.py @@ -20,7 +20,7 @@ def getInterpolators(wm_file, kind='pointwise', shared=False): """ Read 3D gridded data from a processed weather model file and wrap it with - the scipy RegularGridInterpolator + the scipy RegularGridInterpolator. The interpolator grid is (y, x, z) """ @@ -60,9 +60,7 @@ def getInterpolators(wm_file, kind='pointwise', shared=False): def make_shared_raw(inarr): - """ - Make numpy view array of mp.Array - """ + """Make numpy view array of mp.Array.""" # Create flat shared array if mp is None: raise ImportError('multiprocessing is not available') diff --git a/tools/RAiDER/getStationDelays.py b/tools/RAiDER/getStationDelays.py index f0c9d65f9..88fffcb96 100644 --- a/tools/RAiDER/getStationDelays.py +++ b/tools/RAiDER/getStationDelays.py @@ -19,7 +19,7 @@ from RAiDER.logger import logger -def get_delays_UNR(stationFile, filename, dateList, returnTime=None): +def get_delays_UNR(stationFile, filename, dateList, returnTime=None) -> None: """ Parses and returns a dictionary containing either (1) all the GPS delays, if returnTime is None, or (2) only the delay @@ -174,10 +174,8 @@ def get_delays_UNR(stationFile, filename, dateList, returnTime=None): return -def get_station_data(inFile, dateList, gps_repo=None, numCPUs=8, outDir=None, returnTime=None): - """ - Pull tropospheric delay data for a given station name - """ +def get_station_data(inFile, dateList, gps_repo=None, numCPUs=8, outDir=None, returnTime=None) -> None: + """Pull tropospheric delay data for a given station name.""" if outDir is None: outDir = os.getcwd() @@ -244,24 +242,18 @@ def get_station_data(inFile, dateList, gps_repo=None, numCPUs=8, outDir=None, re def get_date(stationFile): - """ - extract the date from a station delay file - """ + """Extract the date from a station delay file.""" # find the date info year = int(stationFile[1]) doy = int(stationFile[2]) date = dt.datetime(year, 1, 1) + dt.timedelta(doy - 1) - return date, year, doy def seconds_of_day(returnTime): - """ - Convert HH:MM:SS format time-tag to seconds of day. - """ + """Convert HH:MM:SS format time-tag to seconds of day.""" if isinstance(returnTime, dt.time): h, m, s = returnTime.hour, returnTime.minute, returnTime.second else: h, m, s = map(int, returnTime.split(":")) - return h * 3600 + m * 60 + s diff --git a/tools/RAiDER/gnss/downloadGNSSDelays.py b/tools/RAiDER/gnss/downloadGNSSDelays.py index c27f9dbc4..193788151 100755 --- a/tools/RAiDER/gnss/downloadGNSSDelays.py +++ b/tools/RAiDER/gnss/downloadGNSSDelays.py @@ -29,7 +29,7 @@ def get_station_list( writeStationFile=True, ): """ - Creates a list of stations inside a lat/lon bounding box from a source + Creates a list of stations inside a lat/lon bounding box from a source. Args: bbox: list of float - length-4 list of floats that describes a bounding box. @@ -104,7 +104,7 @@ def download_tropo_delays( writeDir='.', numCPUs=8, download=False, -): +) -> None: """ Check for and download GNSS tropospheric delays from an archive. If download is True then files will be physically downloaded, but this @@ -155,7 +155,7 @@ def download_tropo_delays( def download_UNR(statID, year, writeDir='.', download=False, baseURL=_UNR_URL): """ - Download a zip file containing tropospheric delays for a given station and year + Download a zip file containing tropospheric delays for a given station and year. The URL format is http://geodesy.unr.edu/gps_timeseries/trop//..trop.zip Inputs: statID - 4-character station identifier @@ -180,7 +180,7 @@ def download_UNR(statID, year, writeDir='.', download=False, baseURL=_UNR_URL): def download_url(url, save_path, chunk_size=2048): """ Download a file from a URL. Modified from - https://stackoverflow.com/questions/9419162/download-returned-zip-file-from-url + https://stackoverflow.com/questions/9419162/download-returned-zip-file-from-url. """ session = requests_retry_session() r = session.get(url, stream=True) @@ -199,7 +199,7 @@ def download_url(url, save_path, chunk_size=2048): def check_url(url): """ Check whether a file exists at a URL. Modified from - https://stackoverflow.com/questions/9419162/download-returned-zip-file-from-url + https://stackoverflow.com/questions/9419162/download-returned-zip-file-from-url. """ session = requests_retry_session() r = session.head(url) @@ -209,16 +209,12 @@ def check_url(url): def in_box(lat, lon, llhbox): - """ - Checks whether the given lat, lon pair are inside the bounding box llhbox - """ + """Checks whether the given lat, lon pair are inside the bounding box llhbox.""" return lat < llhbox[1] and lat > llhbox[0] and lon < llhbox[3] and lon > llhbox[2] def fix_lons(lon): - """ - Fix the given longitudes into the range `[-180, 180]`. - """ + """Fix the given longitudes into the range `[-180, 180]`.""" fixed_lon = ((lon + 180) % 360) - 180 # Make the positive 180s positive again. if fixed_lon == -180 and lon > 0: @@ -227,17 +223,13 @@ def fix_lons(lon): def get_ID(line): - """ - Pulls the station ID, lat, lon, and height for a given entry in the UNR text file - """ + """Pulls the station ID, lat, lon, and height for a given entry in the UNR text file.""" stat_id, lat, lon, height = line.split()[:4] return stat_id, float(lat), float(lon), float(height) -def main(inps=None): - """ - Main workflow for querying supported GPS repositories for zenith delay information. - """ +def main(inps=None) -> None: + """Main workflow for querying supported GPS repositories for zenith delay information.""" try: dateList = inps.date_list returnTime = inps.time @@ -299,9 +291,7 @@ def main(inps=None): def parse_bbox(bounding_box): - """ - Parse bounding box arguments - """ + """Parse bounding box arguments.""" if isinstance(bounding_box, str) and not os.path.isfile(bounding_box): try: bbox = [float(val) for val in bounding_box.split()] @@ -328,9 +318,7 @@ def parse_bbox(bounding_box): def get_stats(bbox, long_cross_zero, out, station_file): - """ - Pull the stations needed - """ + """Pull the stations needed.""" if long_cross_zero == 1: bbox1 = bbox.copy() bbox2 = bbox.copy() @@ -359,7 +347,7 @@ def get_stats(bbox, long_cross_zero, out, station_file): def filterToBBox(stations, llhBox): """ Filter a dataframe by lat/lon. - *NOTE: llhBox longitude format should be [0, 360] + *NOTE: llhBox longitude format should be [0, 360]. Args: stations: DataFrame - a pandas dataframe with "Lat" and "Lon" columns diff --git a/tools/RAiDER/gnss/processDelayFiles.py b/tools/RAiDER/gnss/processDelayFiles.py index e6fe5814f..b6fe3d72f 100644 --- a/tools/RAiDER/gnss/processDelayFiles.py +++ b/tools/RAiDER/gnss/processDelayFiles.py @@ -13,7 +13,7 @@ pd.options.mode.chained_assignment = None # default='warn' -def combineDelayFiles(outName, loc=os.getcwd(), source='model', ext='.csv', ref=None, col_name='ZTD'): +def combineDelayFiles(outName, loc=os.getcwd(), source='model', ext='.csv', ref=None, col_name='ZTD') -> None: files = glob.glob(os.path.join(loc, '*' + ext)) if source == 'model': @@ -53,8 +53,8 @@ def combineDelayFiles(outName, loc=os.getcwd(), source='model', ext='.csv', ref= ) -def addDateTimeToFiles(fileList, force=False, verbose=False): - """Run through a list of files and add the datetime of each file as a column""" +def addDateTimeToFiles(fileList, force=False, verbose=False) -> None: + """Run through a list of files and add the datetime of each file as a column.""" print('Adding Datetime to delay files') for f in tqdm(fileList): @@ -85,7 +85,7 @@ def addDateTimeToFiles(fileList, force=False, verbose=False): def getDateTime(filename): - """Parse a datetime from a RAiDER delay filename""" + """Parse a datetime from a RAiDER delay filename.""" filename = os.path.basename(filename) dtr = re.compile(r'\d{8}T\d{6}') dt = dtr.search(filename) @@ -96,7 +96,7 @@ def getDateTime(filename): def update_time(row, localTime_hrs): - """Update with local origin time""" + """Update with local origin time.""" localTime_estimate = row['Datetime'].replace(hour=localTime_hrs, minute=0, second=0) # determine if you need to shift days @@ -121,7 +121,7 @@ def update_time(row, localTime_hrs): def pass_common_obs(reference, target, localtime=None): - """Pass only observations in target spatiotemporally common to reference""" + """Pass only observations in target spatiotemporally common to reference.""" if isinstance(target['Datetime'].iloc[0], str): target['Datetime'] = target['Datetime'].apply(lambda x: datetime.datetime.strptime(x, '%Y-%m-%d %H:%M:%S')) @@ -147,7 +147,7 @@ def concatDelayFiles( ): """ Read a list of .csv files containing the same columns and append them - together, sorting by specified columns + together, sorting by specified columns. """ dfList = [] @@ -186,9 +186,7 @@ def concatDelayFiles( def local_time_filter(raiderFile, ztdFile, dfr, dfz, localTime): - """ - Convert to local-time reference frame WRT 0 longitude - """ + """Convert to local-time reference frame WRT 0 longitude.""" localTime_hrs = int(localTime.split(' ')[0]) localTime_hrthreshold = int(localTime.split(' ')[1]) # with rotation rate and distance to 0 lon, get localtime shift WRT 00 UTC at 0 lon @@ -243,9 +241,7 @@ def local_time_filter(raiderFile, ztdFile, dfr, dfz, localTime): def readZTDFile(filename, col_name='ZTD'): - """ - Read and parse a GPS zenith delay file - """ + """Read and parse a GPS zenith delay file.""" try: data = pd.read_csv(filename, parse_dates=['Date']) times = data['times'].apply(lambda x: datetime.timedelta(seconds=x)) @@ -356,9 +352,7 @@ def create_parser(): def main(raiderFile, ztdFile, col_name='ZTD', raider_delay='totalDelay', outName=None, localTime=None): - """ - Merge a combined RAiDER delays file with a GPS ZTD delay file - """ + """Merge a combined RAiDER delays file with a GPS ZTD delay file.""" print(f'Merging delay files {raiderFile} and {ztdFile}') dfr = pd.read_csv(raiderFile, parse_dates=['Datetime']) # drop extra columns diff --git a/tools/RAiDER/interpolator.py b/tools/RAiDER/interpolator.py index 4098c12c6..b7846778d 100644 --- a/tools/RAiDER/interpolator.py +++ b/tools/RAiDER/interpolator.py @@ -25,7 +25,7 @@ def __init__( fill_value=None, assume_sorted=False, max_threads=8 - ): + ) -> None: self.grid = grid self.values = values self.fill_value = fill_value @@ -77,9 +77,7 @@ def interp_along_axis(oldCoord, newCoord, data, axis=2, pad=False): def interpV(y, old_x, new_x, left=None, right=None, period=None): - """ - Rearrange np.interp's arguments - """ + """Rearrange np.interp's arguments.""" return np.interp(new_x, old_x, y, left=left, right=right, period=period) @@ -120,7 +118,7 @@ def fillna3D(array, axis=-1, fill_value=0.): def interpolateDEM(demFile, outLL, method='nearest'): - """Interpolate a DEM raster to a set of lat/lon query points using rioxarray + """Interpolate a DEM raster to a set of lat/lon query points using rioxarray. outLL will be a tuple of (lats, lons). lats/lons can either be 1D arrays or 2 For now will only use first row/col of 2D diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index 452087228..0c6634dc7 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -32,7 +32,7 @@ class AOI: _proj - pyproj-compatible CRS _type - Type of AOI """ - def __init__(self): + def __init__(self) -> None: self._output_directory = os.getcwd() self._bounding_box = None self._proj = CRS.from_epsg(4326) @@ -57,7 +57,7 @@ def projection(self): def get_output_spacing(self, crs=4326): - """Return the output spacing in desired units""" + """Return the output spacing in desired units.""" output_spacing_deg = self._output_spacing if not isinstance(crs, CRS): crs = CRS.from_epsg(crs) @@ -71,8 +71,8 @@ def get_output_spacing(self, crs=4326): return output_spacing - def set_output_spacing(self, ll_res=None): - """Calculate the spacing for the output grid and weather model + def set_output_spacing(self, ll_res=None) -> None: + """Calculate the spacing for the output grid and weather model. Use the requested spacing if exists or the weather model grid itself @@ -89,7 +89,7 @@ def set_output_spacing(self, ll_res=None): self._output_spacing = out_spacing - def add_buffer(self, ll_res, digits=2): + def add_buffer(self, ll_res, digits=2) -> None: """ Add a fixed buffer to the AOI, accounting for the cube spacing. @@ -173,13 +173,12 @@ def calc_buffer_ray(self, direction, lookDir='right', incAngle=30, maxZ=80, digi return bounds - def set_output_directory(self, output_directory): + def set_output_directory(self, output_directory) -> None: self._output_directory = output_directory - return - def set_output_xygrid(self, dst_crs=4326): - """Define the locations where the delays will be returned""" + def set_output_xygrid(self, dst_crs=4326) -> None: + """Define the locations where the delays will be returned.""" from RAiDER.utilFcns import transform_bbox try: @@ -202,8 +201,8 @@ def set_output_xygrid(self, dst_crs=4326): class StationFile(AOI): - """Use a .csv file containing at least Lat, Lon, and optionally Hgt_m columns""" - def __init__(self, station_file, demFile=None): + """Use a .csv file containing at least Lat, Lon, and optionally Hgt_m columns.""" + def __init__(self, station_file, demFile=None) -> None: super().__init__() self._filename = station_file self._demfile = demFile @@ -212,15 +211,13 @@ def __init__(self, station_file, demFile=None): def readLL(self): - """Read the station lat/lons from the csv file""" + """Read the station lat/lons from the csv file.""" df = pd.read_csv(self._filename).drop_duplicates(subset=["Lat", "Lon"]) return df['Lat'].values, df['Lon'].values def readZ(self): - """ - Read the station heights from the file, or download a DEM if not present - """ + """Read the station heights from the file, or download a DEM if not present.""" df = pd.read_csv(self._filename).drop_duplicates(subset=["Lat", "Lon"]) if 'Hgt_m' in df.columns: return df['Hgt_m'].values @@ -252,10 +249,8 @@ def readZ(self): class RasterRDR(AOI): - """ - Use a 2-band raster file containing lat/lon coordinates. - """ - def __init__(self, lat_file, lon_file=None, hgt_file=None, dem_file=None, convention='isce'): + """Use a 2-band raster file containing lat/lon coordinates.""" + def __init__(self, lat_file, lon_file=None, hgt_file=None, dem_file=None, convention='isce') -> None: super().__init__() self._type = 'radar_rasters' self._latfile = lat_file @@ -290,9 +285,7 @@ def readLL(self): def readZ(self): - """ - Read the heights from the raster file, or download a DEM if not present - """ + """Read the heights from the raster file, or download a DEM if not present.""" if self._hgtfile is not None and os.path.exists(self._hgtfile): logger.info('Using existing heights at: %s', self._hgtfile) return rio_open(self._hgtfile) @@ -316,16 +309,16 @@ def readZ(self): class BoundingBox(AOI): - """Parse a bounding box AOI""" - def __init__(self, bbox): + """Parse a bounding box AOI.""" + def __init__(self, bbox) -> None: AOI.__init__(self) self._bounding_box = bbox self._type = 'bounding_box' class GeocodedFile(AOI): - """Parse a Geocoded file for coordinates""" - def __init__(self, filename, is_dem=False): + """Parse a Geocoded file for coordinates.""" + def __init__(self, filename, is_dem=False) -> None: super().__init__() from RAiDER.utilFcns import rio_extents, rio_profile @@ -355,9 +348,7 @@ def readLL(self): def readZ(self): - """ - Download a DEM for the file - """ + """Download a DEM for the file.""" from RAiDER.dem import download_dem from RAiDER.interpolator import interpolateDEM @@ -370,8 +361,8 @@ def readZ(self): class Geocube(AOI): - """Pull lat/lon/height from a georeferenced data cube""" - def __init__(self, path_cube): + """Pull lat/lon/height from a georeferenced data cube.""" + def __init__(self, path_cube) -> None: super().__init__() self.path = path_cube self._type = 'Geocube' @@ -401,7 +392,7 @@ def readZ(self): def bounds_from_latlon_rasters(latfile, lonfile): """ Parse lat/lon/height inputs and return - the appropriate outputs + the appropriate outputs. """ from RAiDER.utilFcns import get_file_and_band latinfo = get_file_and_band(latfile) @@ -429,7 +420,7 @@ def bounds_from_latlon_rasters(latfile, lonfile): def bounds_from_csv(station_file): """ station_file should be a comma-delimited file with at least "Lat" - and "Lon" columns, which should be EPSG: 4326 projection (i.e WGS84) + and "Lon" columns, which should be EPSG: 4326 projection (i.e WGS84). """ stats = pd.read_csv(station_file).drop_duplicates(subset=["Lat", "Lon"]) snwe = [stats['Lat'].min(), stats['Lat'].max(), stats['Lon'].min(), stats['Lon'].max()] diff --git a/tools/RAiDER/logger.py b/tools/RAiDER/logger.py index cb3951a3f..4ed748420 100644 --- a/tools/RAiDER/logger.py +++ b/tools/RAiDER/logger.py @@ -5,9 +5,7 @@ # RESERVED. United States Government Sponsorship acknowledged. # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -""" -Global logging configuration -""" +"""Global logging configuration.""" import logging import os import sys @@ -30,7 +28,7 @@ class UnixColorFormatter(Formatter): logging.CRITICAL: bold_red } - def __init__(self, fmt=None, datefmt=None, style="%", use_color=True): + def __init__(self, fmt=None, datefmt=None, style="%", use_color=True) -> None: super().__init__(fmt, datefmt, style) # Save the old function so we can call it later self.__formatMessage = self.formatMessage diff --git a/tools/RAiDER/losreader.py b/tools/RAiDER/losreader.py index 4a05394c1..271d6344d 100644 --- a/tools/RAiDER/losreader.py +++ b/tools/RAiDER/losreader.py @@ -11,7 +11,7 @@ import shelve from abc import ABC from pathlib import PosixPath -from typing import Union +from typing import Literal, NoReturn, Union import numpy as np @@ -30,10 +30,8 @@ class LOS(ABC): - """ - LOS Class definition for handling look vectors - """ - def __init__(self): + """LOS Class definition for handling look vectors.""" + def __init__(self) -> None: self._lats, self._lons, self._heights = None, None, None self._look_vecs = None self._ray_trace = False @@ -41,8 +39,8 @@ def __init__(self): self._is_projected = False - def setPoints(self, lats, lons=None, heights=None): - """Set the pixel locations""" + def setPoints(self, lats, lons=None, heights=None) -> None: + """Set the pixel locations.""" if (lats is None) and (self._lats is None): raise RuntimeError("You haven't given any point locations yet") @@ -62,7 +60,7 @@ def setPoints(self, lats, lons=None, heights=None): self._heights = heights - def setTime(self, dt): + def setTime(self, dt) -> None: self._time = dt @@ -79,16 +77,14 @@ def ray_trace(self): class Zenith(LOS): - """ - Class definition for a "Zenith" object. - """ - def __init__(self): + """Class definition for a "Zenith" object.""" + def __init__(self) -> None: super().__init__() self._is_zenith = True - def setLookVectors(self): - """Set point locations and calculate Zenith look vectors""" + def setLookVectors(self) -> None: + """Set point locations and calculate Zenith look vectors.""" if self._lats is None: raise ValueError('Target points not set') if self._look_vecs is None: @@ -96,7 +92,7 @@ def setLookVectors(self): def __call__(self, delays): - """Placeholder method for consistency with the other classes""" + """Placeholder method for consistency with the other classes.""" return delays @@ -105,7 +101,7 @@ class Conventional(LOS): Special value indicating that the zenith delay will be projected using the standard cos(inc) scaling. """ - def __init__(self, filename=None, los_convention='isce', time=None, pad=600): + def __init__(self, filename=None, los_convention='isce', time=None, pad=600) -> None: super().__init__() self._file = filename self._time = time @@ -117,9 +113,7 @@ def __init__(self, filename=None, los_convention='isce', time=None, pad=600): def __call__(self, delays): - """ - Read the LOS file and convert it to look vectors - """ + """Read the LOS file and convert it to look vectors.""" if self._lats is None: raise ValueError('Target points not set') if self._file is None: @@ -182,8 +176,8 @@ class Raytracing(LOS): >>> import numpy as np """ - def __init__(self, filename=None, los_convention='isce', time=None, look_dir = 'right', pad=600): - """Read in and parse a statevector file""" + def __init__(self, filename=None, los_convention='isce', time=None, look_dir = 'right', pad=600) -> None: + """Read in and parse a statevector file.""" if isce is None: raise ImportError('isce3 is required for this class. Use conda to install isce3`') @@ -211,7 +205,7 @@ def __init__(self, filename=None, los_convention='isce', time=None, look_dir = ' raise RuntimeError(f"Unknown look direction: {look_dir}") - def getSensorDirection(self): + def getSensorDirection(self) -> Literal['desc', 'asc']: if self._orbit is None: raise ValueError('The orbit has not been set') z = self._orbit.position[:,2] @@ -228,15 +222,13 @@ def getLookDirection(self): return self._look_dir # Called in checkArgs - def setTime(self, time, pad=600): + def setTime(self, time, pad=600) -> None: self._time = time self._orbit = get_orbit(self._file, self._time, pad=pad) def getLookVectors(self, ht, llh, xyz, yy): - """ - Calculate look vectors for raytracing - """ + """Calculate look vectors for raytracing.""" if isce is None: raise ImportError('isce3 is required for this method. Use conda to install isce3`') @@ -271,7 +263,7 @@ def getLookVectors(self, ht, llh, xyz, yy): def getIntersectionWithHeight(self, height): """ This function computes the intersection point of a ray at a height - level + level. """ # We just leverage the same code as finding top of atmosphere here return getTopOfAtmosphere(self._xyz, self._look_vecs, height) @@ -303,7 +295,7 @@ def getIntersectionWithLevels(self, levels): return rays - def calculateDelays(self, delays): + def calculateDelays(self, delays) -> NoReturn: """ Here "delays" is point-wise delays (i.e. refractivities), not integrated ZTD/STD. @@ -336,7 +328,7 @@ def get_sv(los_file: Union[str, list, PosixPath], ref_time: datetime.datetime, pad: int): """ - Read an LOS file and return orbital state vectors + Read an LOS file and return orbital state vectors. Args: los_file (str, Path, list): - user-passed file containing either look @@ -496,7 +488,7 @@ def read_txt_file(filename): def read_ESA_Orbit_file(filename): """ - Read orbit data from an orbit file supplied by ESA + Read orbit data from an orbit file supplied by ESA. Args: ---------- @@ -543,7 +535,7 @@ def read_ESA_Orbit_file(filename): def pick_ESA_orbit_file(list_files:list, ref_time:datetime.datetime): - """From list of .EOF orbit files, pick the one that contains 'ref_time'""" + """From list of .EOF orbit files, pick the one that contains 'ref_time'.""" orb_file = None for path in list_files: f = os.path.basename(path) @@ -560,7 +552,7 @@ def pick_ESA_orbit_file(list_files:list, ref_time:datetime.datetime): def filter_ESA_orbit_file(orbit_xml: str, ref_time: datetime.datetime) -> bool: - """Returns true or false depending on whether orbit file contains ref time + """Returns true or false depending on whether orbit file contains ref time. Parameters ---------- @@ -773,7 +765,7 @@ def get_orbit(orbit_file: Union[list, str], for the sensor (can be download with sentineleof libray). Lists of files are only accepted for Sentinel-1 EOF files. pad (int): - number of seconds to keep around the - requested time (should be about 600 seconds) + requested time (should be about 600 seconds). """ if isce is None: @@ -804,7 +796,7 @@ def get_orbit(orbit_file: Union[list, str], def build_ray(model_zs, ht, xyz, LOS, MAX_TROPO_HEIGHT=_ZREF): """ - Compute the ray length in ECEF between each weather model layers + Compute the ray length in ECEF between each weather model layers. Only heights up to MAX_TROPO_HEIGHT are considered Assumption: model_zs (model) are assumed to be sorted in height diff --git a/tools/RAiDER/models/credentials.py b/tools/RAiDER/models/credentials.py index 9c54b76a4..a0f8c2d20 100644 --- a/tools/RAiDER/models/credentials.py +++ b/tools/RAiDER/models/credentials.py @@ -1,6 +1,6 @@ """ API credential information and help url for downloading weather model data - saved in a hidden file in home directory + saved in a hidden file in home directory. api filename weather models UID KEY URL _________________________________________________________________________________ @@ -157,6 +157,6 @@ def check_api(model: str, rc_path.chmod(0o000600) -def setup_from_env(): +def setup_from_env() -> None: for model in RC_FILENAMES.keys(): check_api(model) diff --git a/tools/RAiDER/models/customExceptions.py b/tools/RAiDER/models/customExceptions.py index fbc9c431b..6c7f29e5b 100644 --- a/tools/RAiDER/models/customExceptions.py +++ b/tools/RAiDER/models/customExceptions.py @@ -1,30 +1,30 @@ class DatetimeFailed(Exception): - def __init__(self, model, time): + def __init__(self, model, time) -> None: msg = f"Weather model {model} failed to download for datetime {time}" super().__init__(msg) class DatetimeNotAvailable(Exception): - def __init__(self, model, time): + def __init__(self, model, time) -> None: msg = f"Weather model {model} was not found for datetime {time}" super().__init__(msg) class DatetimeOutsideRange(Exception): - def __init__(self, model, time): + def __init__(self, model, time) -> None: msg = f"Time {time} is outside the available date range for weather model {model}" super().__init__(msg) class ExistingWeatherModelTooSmall(Exception): - def __init__(self): + def __init__(self) -> None: msg = 'The weather model passed does not cover all of the input ' \ 'points; you may need to download a larger area.' super().__init__(msg) class TryToKeepGoingError(Exception): - def __init__(self, date=None): + def __init__(self, date=None) -> None: if date is not None: msg = 'The weather model does not exist for date {date}, so I will try to use the closest available date.' else: @@ -32,19 +32,19 @@ def __init__(self, date=None): super().__init__(msg) class CriticalError(Exception): - def __init__(self): + def __init__(self) -> None: msg = 'I have experienced a critical error, please take a look at the log files' super().__init__(msg) class WrongNumberOfFiles(Exception): - def __init__(self, Nexp, Navail): + def __init__(self, Nexp, Navail) -> None: msg = 'The number of files downloaded does not match the requested, ' f'I expected {Nexp} and got {Navail}, aborting' super().__init__(msg) class NoWeatherModelData(Exception): - def __init__(self, custom_msg=None): + def __init__(self, custom_msg=None) -> None: if custom_msg is None: msg = 'No weather model files were available to download, aborting' else: @@ -53,7 +53,7 @@ def __init__(self, custom_msg=None): class NoStationDataFoundError(Exception): - def __init__(self, station_list=None, years=None): + def __init__(self, station_list=None, years=None) -> None: if (station_list is None) and (years is None): msg = 'No GNSS station data was found' elif (years is None): diff --git a/tools/RAiDER/models/ecmwf.py b/tools/RAiDER/models/ecmwf.py index f12afb1cc..7468ed973 100755 --- a/tools/RAiDER/models/ecmwf.py +++ b/tools/RAiDER/models/ecmwf.py @@ -16,11 +16,9 @@ class ECMWF(WeatherModel): - """ - Implement ECMWF models - """ + """Implement ECMWF models.""" - def __init__(self): + def __init__(self) -> None: # initialize a weather model WeatherModel.__init__(self) @@ -50,7 +48,7 @@ def __model_levels__(self): self._b = B_137_HRES - def load_weather(self, f=None, *args, **kwargs): + def load_weather(self, f=None, *args, **kwargs) -> None: """ Consistent class method to be implemented across all weather model types. As a result of calling this method, all of the variables (x, y, z, p, q, @@ -61,7 +59,7 @@ def load_weather(self, f=None, *args, **kwargs): self._load_model_level(f) - def _load_model_level(self, fname): + def _load_model_level(self, fname) -> None: # read data from netcdf file lats, lons, xs, ys, t, q, lnsp, z = self._makeDataCubes( fname, @@ -120,10 +118,8 @@ def _load_model_level(self, fname): self._zs = np.flip(h, axis=2) - def _fetch(self, out): - """ - Fetch a weather model from ECMWF - """ + def _fetch(self, out) -> None: + """Fetch a weather model from ECMWF.""" # bounding box plus a buffer lat_min, lat_max, lon_min, lon_max = self._ll_bounds @@ -142,7 +138,7 @@ def _fetch(self, out): def _get_from_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, - lon_step, time, out): + lon_step, time, out) -> None: import ecmwfapi server = ecmwfapi.ECMWFDataServer() @@ -190,8 +186,8 @@ def _get_from_cds( lon_max, acqTime, outname - ): - """Used for ERA5""" + ) -> None: + """Used for ERA5.""" import cdsapi c = cdsapi.Client(verify=0) @@ -233,8 +229,8 @@ def _get_from_cds( raise Exception - def _download_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step, time, out): - """Used for HRES""" + def _download_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step, time, out) -> None: + """Used for HRES.""" from ecmwfapi import ECMWFService server = ECMWFService("mars") @@ -271,7 +267,7 @@ def _download_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step ) - def _load_pressure_level(self, filename, *args, **kwargs): + def _load_pressure_level(self, filename, *args, **kwargs) -> None: with xr.open_dataset(filename) as block: # Pull the data z = np.squeeze(block['z'].values) @@ -329,7 +325,7 @@ def _load_pressure_level(self, filename, *args, **kwargs): def _makeDataCubes(self, fname, verbose=False): """ Create a cube of data representing temperature and relative humidity - at specified pressure levels + at specified pressure levels. """ # get ll_bounds S, N, W, E = self._ll_bounds diff --git a/tools/RAiDER/models/era5.py b/tools/RAiDER/models/era5.py index 62918d356..182a5f8f0 100755 --- a/tools/RAiDER/models/era5.py +++ b/tools/RAiDER/models/era5.py @@ -9,7 +9,7 @@ class ERA5(ECMWF): # I took this from # https://www.ecmwf.int/en/forecasts/documentation-and-support/137-model-levels. - def __init__(self): + def __init__(self) -> None: ECMWF.__init__(self) self._humidityType = 'q' @@ -34,10 +34,8 @@ def __init__(self): self.setLevelType('ml') - def _fetch(self, out): - """ - Fetch a weather model from ECMWF - """ + def _fetch(self, out) -> None: + """Fetch a weather model from ECMWF.""" # bounding box plus a buffer lat_min, lat_max, lon_min, lon_max = self._ll_bounds time = self._time @@ -46,8 +44,8 @@ def _fetch(self, out): self._get_from_cds(lat_min, lat_max, lon_min, lon_max, time, out) - def load_weather(self, f=None, *args, **kwargs): - """Load either pressure or model level data""" + def load_weather(self, f=None, *args, **kwargs) -> None: + """Load either pressure or model level data.""" f = self.files[0] if f is None else f if self._model_level_type == 'pl': self._load_pressure_level(f, *args, **kwargs) diff --git a/tools/RAiDER/models/era5t.py b/tools/RAiDER/models/era5t.py index 456bdee92..42b602faa 100644 --- a/tools/RAiDER/models/era5t.py +++ b/tools/RAiDER/models/era5t.py @@ -6,7 +6,7 @@ class ERA5T(ERA5): # I took this from # https://www.ecmwf.int/en/forecasts/documentation-and-support/137-model-levels. - def __init__(self): + def __init__(self) -> None: ERA5.__init__(self) self._expver = '0005' diff --git a/tools/RAiDER/models/erai.py b/tools/RAiDER/models/erai.py index b91267bf1..d120d2219 100755 --- a/tools/RAiDER/models/erai.py +++ b/tools/RAiDER/models/erai.py @@ -7,7 +7,7 @@ class ERAI(ECMWF): # A and B parameters to calculate pressures for model levels, # extracted from an ECMWF ERA-Interim GRIB file and then hardcoded here - def __init__(self): + def __init__(self) -> None: ECMWF.__init__(self) self._classname = 'ei' self._expver = '0001' diff --git a/tools/RAiDER/models/generateGACOSVRT.py b/tools/RAiDER/models/generateGACOSVRT.py index a449569a1..911f7b925 100644 --- a/tools/RAiDER/models/generateGACOSVRT.py +++ b/tools/RAiDER/models/generateGACOSVRT.py @@ -3,19 +3,15 @@ # Copyright 2018 -def makeVRT(filename, dtype='Float32'): - """ - Use an RSC file to create a GDAL-compatible VRT file for opening GACOS weather model files - """ +def makeVRT(filename, dtype='Float32') -> None: + """Use an RSC file to create a GDAL-compatible VRT file for opening GACOS weather model files.""" fields = readRSC(filename) string = vrtStr(fields['XMAX'], fields['YMAX'], fields['X_FIRST'], fields['Y_FIRST'], fields['X_STEP'], fields['Y_STEP'], filename.replace('.rsc', ''), dtype=dtype) writeStringToFile(string, filename.replace('.rsc', '').replace('.ztd', '') + '.vrt') def writeStringToFile(string, filename): - """ - Write a string to a VRT file - """ + """Write a string to a VRT file.""" with open(filename, 'w') as f: f.write(string) @@ -42,17 +38,15 @@ def vrtStr(xSize, ySize, lon1, lat1, lonStep, latStep, filename, dtype='Float32' return string -def convertAllFiles(dirLoc): - """ - convert all RSC files to VRT files contained in dirLoc - """ +def convertAllFiles(dirLoc) -> None: + """Convert all RSC files to VRT files contained in dirLoc.""" import glob files = glob.glob('*.rsc') for f in files: makeVRT(f) -def main(): +def main() -> None: import sys if len(sys.argv) == 2: makeVRT(sys.argv[1]) diff --git a/tools/RAiDER/models/gmao.py b/tools/RAiDER/models/gmao.py index e03ecab9d..8e22f79ea 100755 --- a/tools/RAiDER/models/gmao.py +++ b/tools/RAiDER/models/gmao.py @@ -19,7 +19,7 @@ class GMAO(WeatherModel): # I took this from GMAO model level weblink # https://opendap.nccs.nasa.gov/dods/GEOS-5/fp/0.25_deg/assim/inst3_3d_asm_Nv - def __init__(self): + def __init__(self) -> None: # initialize a weather model WeatherModel.__init__(self) @@ -58,10 +58,8 @@ def __init__(self): self._proj = CRS.from_epsg(4326) - def _fetch(self, out): - """ - Fetch weather model data from GMAO - """ + def _fetch(self, out) -> None: + """Fetch weather model data from GMAO.""" acqTime = self._time # calculate the array indices for slicing the GMAO variable arrays @@ -146,7 +144,7 @@ def _fetch(self, out): logger.exception("Unable to save weathermodel to file") - def load_weather(self, f=None): + def load_weather(self, f=None) -> None: """ Consistent class method to be implemented across all weather model types. As a result of calling this method, all of the variables (x, y, z, p, q, @@ -157,10 +155,8 @@ def load_weather(self, f=None): self._load_model_level(f) - def _load_model_level(self, filename): - """ - Get the variables from the GMAO link using OpenDAP - """ + def _load_model_level(self, filename) -> None: + """Get the variables from the GMAO link using OpenDAP.""" # adding the import here should become absolute when transition to netcdf from netCDF4 import Dataset with Dataset(filename, mode='r') as f: diff --git a/tools/RAiDER/models/hres.py b/tools/RAiDER/models/hres.py index 21dfe7108..f8f1807e7 100755 --- a/tools/RAiDER/models/hres.py +++ b/tools/RAiDER/models/hres.py @@ -13,11 +13,9 @@ class HRES(ECMWF): - """ - Implement ECMWF models - """ + """Implement ECMWF models.""" - def __init__(self, level_type='ml'): + def __init__(self, level_type='ml') -> None: # initialize a weather model WeatherModel.__init__(self) @@ -50,7 +48,7 @@ def __init__(self, level_type='ml'): self.setLevelType('ml') - def update_a_b(self): + def update_a_b(self) -> None: # Before 2013-06-26, there were only 91 model levels. The mapping coefficients below are extracted # based on https://www.ecmwf.int/en/forecasts/documentation-and-support/91-model-levels self._levels = 91 @@ -58,7 +56,7 @@ def update_a_b(self): self._a = A_91_HRES self._b = B_91_HRES - def load_weather(self, f=None): + def load_weather(self, f=None) -> None: """ Consistent class method to be implemented across all weather model types. As a result of calling this method, all of the variables (x, y, z, p, q, @@ -74,10 +72,8 @@ def load_weather(self, f=None): elif self._model_level_type == 'pl': self._load_pressure_levels(f) - def _fetch(self,out): - """ - Fetch a weather model from ECMWF - """ + def _fetch(self,out) -> None: + """Fetch a weather model from ECMWF.""" # bounding box plus a buffer lat_min, lat_max, lon_min, lon_max = self._ll_bounds time = self._time diff --git a/tools/RAiDER/models/hrrr.py b/tools/RAiDER/models/hrrr.py index 71361e6f4..ecf8d2554 100644 --- a/tools/RAiDER/models/hrrr.py +++ b/tools/RAiDER/models/hrrr.py @@ -24,7 +24,7 @@ def check_hrrr_dataset_availability(dt: datetime) -> bool: - """Note a file could still be missing within the models valid range""" + """Note a file could still be missing within the models valid range.""" H = Herbie(dt, model='hrrr', product='nat', @@ -32,9 +32,9 @@ def check_hrrr_dataset_availability(dt: datetime) -> bool: avail = (H.grib_source is not None) return avail -def download_hrrr_file(ll_bounds, DATE, out, model='hrrr', product='nat', fxx=0, verbose=False): +def download_hrrr_file(ll_bounds, DATE, out, model='hrrr', product='nat', fxx=0, verbose=False) -> None: """ - Download a HRRR weather model using Herbie + Download a HRRR weather model using Herbie. Args: DATE (Python datetime) - Datetime as a Python datetime. Herbie will automatically return the closest valid time, @@ -121,9 +121,7 @@ def download_hrrr_file(ll_bounds, DATE, out, model='hrrr', product='nat', fxx=0, def get_bounds_indices(SNWE, lats, lons): - """ - Convert SNWE lat/lon bounds to index bounds - """ + """Convert SNWE lat/lon bounds to index bounds.""" # Unpack the bounds and find the relevent indices S, N, W, E = SNWE @@ -161,9 +159,7 @@ def get_bounds_indices(SNWE, lats, lons): def load_weather_hrrr(filename): - """ - Loads a weather model from a HRRR file - """ + """Loads a weather model from a HRRR file.""" # read data from the netcdf file ds = xarray.open_dataset(filename, engine='netcdf4') # Pull the relevant data from the file @@ -190,7 +186,7 @@ def load_weather_hrrr(filename): class HRRR(WeatherModel): - def __init__(self): + def __init__(self) -> None: # initialize a weather model super().__init__() @@ -259,10 +255,8 @@ def __pressure_levels__(self): raise NotImplementedError('Pressure levels do not go high enough for HRRR.') - def _fetch(self, out): - """ - Fetch weather model data from HRRR - """ + def _fetch(self, out) -> None: + """Fetch weather model data from HRRR.""" self._files = out corrected_DT = round_date(self._time, datetime.timedelta(hours=self._time_res)) self.checkTime(corrected_DT) @@ -276,7 +270,7 @@ def _fetch(self, out): download_hrrr_file(bounds, corrected_DT, out, 'hrrr', self._model_level_type) - def load_weather(self, f=None, *args, **kwargs): + def load_weather(self, f=None, *args, **kwargs) -> None: """ Load a weather model into a python weatherModel object, from self.files if no filename is passed. @@ -303,7 +297,7 @@ def load_weather(self, f=None, *args, **kwargs): def checkValidBounds(self: WeatherModel, ll_bounds: np.ndarray): """ Checks whether the given bounding box is valid for the HRRR or HRRRAK - (i.e., intersects with the model domain at all) + (i.e., intersects with the model domain at all). Args: ll_bounds : np.ndarray @@ -337,7 +331,7 @@ def checkValidBounds(self: WeatherModel, ll_bounds: np.ndarray): class HRRRAK(WeatherModel): - def __init__(self): + def __init__(self) -> None: # The HRRR-AK model has a few different parameters than HRRR-CONUS. # These will get used if a user requests a bounding box in Alaska super().__init__() @@ -383,7 +377,7 @@ def __pressure_levels__(self): raise NotImplementedError('hrrr.py: Revisit whether or not pressure levels from HRRR can be used for delay calculations; they do not go high enough compared to native model levels.') - def _fetch(self, out): + def _fetch(self, out) -> None: bounds = self._ll_bounds.copy() bounds[2:] = np.mod(bounds[2:], 360) corrected_DT = round_date(self._time, datetime.timedelta(hours=self._time_res)) @@ -394,7 +388,7 @@ def _fetch(self, out): download_hrrr_file(bounds, corrected_DT, out, 'hrrrak', self._model_level_type) - def load_weather(self, f=None, *args, **kwargs): + def load_weather(self, f=None, *args, **kwargs) -> None: if f is None: f = self.files[0] if isinstance(self.files, list) else self.files _xs, _ys, _lons, _lats, qs, temps, pres, geo_hgt, proj = load_weather_hrrr(f) diff --git a/tools/RAiDER/models/merra2.py b/tools/RAiDER/models/merra2.py index 5c0d50824..ffda1cc29 100755 --- a/tools/RAiDER/models/merra2.py +++ b/tools/RAiDER/models/merra2.py @@ -25,7 +25,7 @@ def Model(): class MERRA2(WeatherModel): - def __init__(self): + def __init__(self) -> None: import calendar # initialize a weather model @@ -66,10 +66,8 @@ def __init__(self): # Projection self._proj = CRS.from_epsg(4326) - def _fetch(self, out): - """ - Fetch weather model data from GMAO: note we only extract the lat/lon bounds for this weather model; fetching data is not needed here as we don't actually download any data using OpenDAP - """ + def _fetch(self, out) -> None: + """Fetch weather model data from GMAO: note we only extract the lat/lon bounds for this weather model; fetching data is not needed here as we don't actually download any data using OpenDAP.""" time = self._time # check whether the file already exists @@ -125,7 +123,7 @@ def _fetch(self, out): logger.exception("MERRA-2: Unable to save weathermodel to file") raise RuntimeError(f'MERRA-2 failed with the following error: {e}') - def load_weather(self, f=None, *args, **kwargs): + def load_weather(self, f=None, *args, **kwargs) -> None: """ Consistent class method to be implemented across all weather model types. As a result of calling this method, all of the variables (x, y, z, p, q, @@ -135,10 +133,8 @@ def load_weather(self, f=None, *args, **kwargs): f = self.files[0] if f is None else f self._load_model_level(f) - def _load_model_level(self, filename): - """ - Get the variables from the GMAO link using OpenDAP - """ + def _load_model_level(self, filename) -> None: + """Get the variables from the GMAO link using OpenDAP.""" # adding the import here should become absolute when transition to netcdf ds = xarray.load_dataset(filename) lons = ds['longitude'].values diff --git a/tools/RAiDER/models/model_levels.py b/tools/RAiDER/models/model_levels.py index c7434d808..549c5d2ae 100644 --- a/tools/RAiDER/models/model_levels.py +++ b/tools/RAiDER/models/model_levels.py @@ -1,5 +1,5 @@ """ -Pre-defined model levels and a, b constants for the different weather models +Pre-defined model levels and a, b constants for the different weather models. **NOTE**: The fixed heights used here are from ECMWF's _geometric_ altitudes (https://confluence.ecmwf.int/display/UDOC/L137+model+level+definitions), diff --git a/tools/RAiDER/models/ncmr.py b/tools/RAiDER/models/ncmr.py index b6fc7e674..5d3b92393 100755 --- a/tools/RAiDER/models/ncmr.py +++ b/tools/RAiDER/models/ncmr.py @@ -22,11 +22,9 @@ class NCMR(WeatherModel): - """ - Implement NCMRWF NCUM (named as NCMR) model in future - """ + """Implement NCMRWF NCUM (named as NCMR) model in future.""" - def __init__(self): + def __init__(self) -> None: # initialize a weather model WeatherModel.__init__(self) @@ -62,10 +60,10 @@ def __init__(self): # Projection self._proj = CRS.from_epsg(4326) - def _fetch(self, out): + def _fetch(self, out) -> None: """ Fetch weather model data from NCMR: note we only extract the lat/lon bounds for this weather model; - fetching data is not needed here as we don't actually download data , data exist in same system + fetching data is not needed here as we don't actually download data , data exist in same system. """ time = self._time @@ -75,10 +73,8 @@ def _fetch(self, out): ''' self._files = self._download_ncmr_file(out, time, self._ll_bounds) - def load_weather(self, f=None, *args, **kwargs): - """ - Load NCMR model variables from existing file - """ + def load_weather(self, f=None, *args, **kwargs) -> None: + """Load NCMR model variables from existing file.""" f = self.files[0] if f is None else f # bounding box plus a buffer @@ -87,10 +83,10 @@ def load_weather(self, f=None, *args, **kwargs): self._makeDataCubes(f) - def _download_ncmr_file(self, out, date_time, bounding_box): + def _download_ncmr_file(self, out, date_time, bounding_box) -> None: """ Download weather model data (whole globe) from NCMR weblink, crop it to the region of interest, and save the cropped data as a standard .nc file of RAiDER (e.g. "NCMR_YYYY_MM_DD_THH_MM_SS.nc"); - Temporarily download data from NCMR ftp 'https://ftp.ncmrwf.gov.in/pub/outgoing/SAC/NCUM_OSF/' and copied in weather_models folder + Temporarily download data from NCMR ftp 'https://ftp.ncmrwf.gov.in/pub/outgoing/SAC/NCUM_OSF/' and copied in weather_models folder. """ from netCDF4 import Dataset @@ -177,10 +173,8 @@ def _download_ncmr_file(self, out, date_time, bounding_box): except Exception: logger.exception("Unable to save weathermodel to file") - def _makeDataCubes(self, filename): - """ - Get the variables from the saved .nc file (named as "NCMR_YYYY_MM_DD_THH_MM_SS.nc") - """ + def _makeDataCubes(self, filename) -> None: + """Get the variables from the saved .nc file (named as "NCMR_YYYY_MM_DD_THH_MM_SS.nc").""" from netCDF4 import Dataset # adding the import here should become absolute when transition to netcdf diff --git a/tools/RAiDER/models/plotWeather.py b/tools/RAiDER/models/plotWeather.py index b101f0da4..1ef096877 100755 --- a/tools/RAiDER/models/plotWeather.py +++ b/tools/RAiDER/models/plotWeather.py @@ -18,9 +18,7 @@ class objects. It is not designed to be used on its own apart def plot_pqt(weatherObj, savefig=True, z1=500, z2=15000): - """ - Create a plot with pressure, temp, and humidity at two heights - """ + """Create a plot with pressure, temp, and humidity at two heights.""" # Get the interpolator intFcn_p = Interpolator((weatherObj._xs, weatherObj._ys, weatherObj._zs), weatherObj._p.swapaxes(0, 1)) @@ -100,7 +98,7 @@ def plot_pqt(weatherObj, savefig=True, z1=500, z2=15000): def plot_wh(weatherObj, savefig=True, z1=500, z2=15000): """ Create a plot with wet refractivity and hydrostatic refractivity, - at two different heights + at two different heights. """ # Get the interpolator intFcn_w = Interpolator((weatherObj._xs, weatherObj._ys, weatherObj._zs), weatherObj._wet_refractivity.swapaxes(0, 1)) diff --git a/tools/RAiDER/models/template.py b/tools/RAiDER/models/template.py index 76c2fc513..905f26140 100644 --- a/tools/RAiDER/models/template.py +++ b/tools/RAiDER/models/template.py @@ -10,7 +10,7 @@ class customModelReader(WeatherModel): - def __init__(self): + def __init__(self) -> None: WeatherModel.__init__(self) self._humidityType = 'q' # can be "q" (specific humidity) or "rh" (relative humidity) self._model_level_type = 'pl' # Default, pressure levels are "pl", and model levels are "ml" @@ -59,7 +59,7 @@ def __init__(self): p1 = CRS(f'+proj=lcc +lat_1={lat1} +lat_2={lat2} +lat_0={lat0} +lon_0={lon0} +x_0={x0} +y_0={y0} +a={earth_radius} +b={earth_radius} +units=m +no_defs') self._proj = p1 - def _fetch(self, out): + def _fetch(self, out) -> None: """ Fetch weather model data from the custom weather model "ABCD" Inputs (no need to change in the custom weather model reader): @@ -67,7 +67,7 @@ def _fetch(self, out): lons - longitude time - datatime object (year,month,day,hour,minute,second) out - name of downloaded dataset file from the custom weather model server - Nextra - buffer of latitude/longitude for determining the bounding box + Nextra - buffer of latitude/longitude for determining the bounding box. """ # Auxilliary function: # download dataset of the custom weather model "ABCD" from a server and then save it to a file named out. @@ -77,11 +77,11 @@ def _fetch(self, out): # retrieval to the following "load_weather" function. self._files = self._download_abcd_file(out, 'abcd', self._time, self._ll_bounds) - def load_weather(self, filename): + def load_weather(self, filename) -> None: """ Load weather model variables from the downloaded file named filename Inputs: - filename - filename of the downloaded weather model file + filename - filename of the downloaded weather model file. """ # Auxilliary function: # read individual variables (in 3-D cube format with exactly the same dimension) from downloaded file @@ -132,7 +132,7 @@ def load_weather(self, filename): ########### - def _download_abcd_file(self, out, model_name, date_time, bounding_box): + def _download_abcd_file(self, out, model_name, date_time, bounding_box) -> None: """ Auxilliary function: Download weather model data from a server @@ -142,11 +142,11 @@ def _download_abcd_file(self, out, model_name, date_time, bounding_box): date_time - datatime object (year,month,day,hour,minute,second) bounding_box - lat/lon bounding box for the region of interest Output: - out - returned filename from input + out - returned filename from input. """ pass - def _makeDataCubes(self, filename): + def _makeDataCubes(self, filename) -> None: """ Auxilliary function: Read 3-D data cubes from downloaded file or directly from weather model weblink (in which case, there is no @@ -161,6 +161,6 @@ def _makeDataCubes(self, filename): t - temperature (3-D data cube) q - humidity (3-D data cube; could be relative humidity or specific humidity) p - pressure level (3-D data cube; could be pressure level (preferred) or surface pressure) - hgt - height (3-D data cube; could be geopotential height or topographic height (preferred)) + hgt - height (3-D data cube; could be geopotential height or topographic height (preferred)). """ pass diff --git a/tools/RAiDER/models/weatherModel.py b/tools/RAiDER/models/weatherModel.py index ee595733c..e8fe83a2b 100755 --- a/tools/RAiDER/models/weatherModel.py +++ b/tools/RAiDER/models/weatherModel.py @@ -30,11 +30,9 @@ } class WeatherModel(ABC): - """ - Implement a generic weather model for getting estimated SAR delays - """ + """Implement a generic weather model for getting estimated SAR delays.""" - def __init__(self): + def __init__(self) -> None: # Initialize model-specific constants/parameters self._k1 = None self._k2 = None @@ -97,7 +95,7 @@ def __init__(self): self._hydrostatic_ztd = None - def __str__(self): + def __str__(self) -> str: string = '\n' string += '======Weather Model class object=====\n' string += f'Weather model time: {self._time}\n' @@ -127,7 +125,7 @@ def __str__(self): string += f'Minimum/Maximum zs/heights: {robmin(self._zs): 10.2f}/{robmax(self._zs): 10.2f}\n'\ string += '=====================================\n' - return str(string) + return string def Model(self): @@ -142,10 +140,10 @@ def getLLRes(self): return np.max([self._lat_res, self._lon_res]) - def fetch(self, out, time): + def fetch(self, out, time) -> None: """ Checks the input datetime against the valid date range for the model and then - calls the model _fetch routine + calls the model _fetch routine. Args: ---------- @@ -166,9 +164,7 @@ def fetch(self, out, time): @abstractmethod def _fetch(self, out): - """ - Placeholder method. Should be implemented in each weather model type class - """ + """Placeholder method. Should be implemented in each weather model type class.""" pass @@ -176,8 +172,8 @@ def getTime(self): return self._time - def setTime(self, time, fmt='%Y-%m-%dT%H:%M:%S'): - """Set the time for a weather model""" + def setTime(self, time, fmt='%Y-%m-%dT%H:%M:%S') -> None: + """Set the time for a weather model.""" if isinstance(time, str): self._time = datetime.datetime.strptime(time, fmt) elif isinstance(time, datetime.datetime): @@ -192,7 +188,7 @@ def get_latlon_bounds(self): return self._ll_bounds - def set_latlon_bounds(self, ll_bounds, Nextra=2, output_spacing=None): + def set_latlon_bounds(self, ll_bounds, Nextra=2, output_spacing=None) -> None: """ Need to correct lat/lon bounds because not all of the weather models have valid data exactly bounded by -90/90 (lats) and -180/180 (lons); for GMAO and MERRA2, @@ -226,7 +222,7 @@ def set_latlon_bounds(self, ll_bounds, Nextra=2, output_spacing=None): def get_wmLoc(self): - """Get the path to the direct with the weather model files""" + """Get the path to the direct with the weather model files.""" if self._wmLoc is None: wmLoc = os.path.join(os.getcwd(), 'weather_files') else: @@ -234,8 +230,8 @@ def get_wmLoc(self): return wmLoc - def set_wmLoc(self, weather_model_directory:str): - """Set the path to the directory with the weather model files""" + def set_wmLoc(self, weather_model_directory:str) -> None: + """Set the path to the directory with the weather model files.""" self._wmLoc = weather_model_directory @@ -275,16 +271,12 @@ def load( @abstractmethod def load_weather(self, *args, **kwargs): - """ - Placeholder method. Should be implemented in each weather model type class - """ + """Placeholder method. Should be implemented in each weather model type class.""" pass def plot(self, plotType='pqt', savefig=True): - """ - Plotting method. Valid plot types are 'pqt' - """ + """Plotting method. Valid plot types are 'pqt'.""" if plotType == 'pqt': plot = plots.plot_pqt(self, savefig) elif plotType == 'wh': @@ -294,9 +286,9 @@ def plot(self, plotType='pqt', savefig=True): return plot - def checkTime(self, time): + def checkTime(self, time) -> None: """ - Checks the time against the lag time and valid date range for the given model type + Checks the time against the lag time and valid date range for the given model type. Parameters: time - Python datetime object @@ -330,8 +322,8 @@ def checkTime(self, time): raise DatetimeOutsideRange(self.Model(), time) - def setLevelType(self, levelType): - """Set the level type to model levels or pressure levels""" + def setLevelType(self, levelType) -> None: + """Set the level type to model levels or pressure levels.""" if levelType in 'ml pl nat prs'.split(): self._model_level_type = levelType else: @@ -344,23 +336,19 @@ def setLevelType(self, levelType): def _convertmb2Pa(self, pres): - """ - Convert pressure in millibars to Pascals - """ + """Convert pressure in millibars to Pascals.""" return 100 * pres - def _get_heights(self, lats, geo_hgt, geo_ht_fill=np.nan): - """ - Transform geo heights to WGS84 ellipsoidal heights - """ + def _get_heights(self, lats, geo_hgt, geo_ht_fill=np.nan) -> None: + """Transform geo heights to WGS84 ellipsoidal heights.""" geo_ht_fix = np.where(geo_hgt != geo_ht_fill, geo_hgt, np.nan) lats_full = np.broadcast_to(lats[...,np.newaxis], geo_ht_fix.shape) self._zs = util.geo_to_ht(lats_full, geo_ht_fix) - def _find_e(self): - """Check the type of e-calculation needed""" + def _find_e(self) -> None: + """Check the type of e-calculation needed.""" if self._humidityType == 'rh': self._find_e_from_rh() elif self._humidityType == 'q': @@ -371,7 +359,7 @@ def _find_e(self): self._q = None - def _find_e_from_q(self): + def _find_e_from_q(self) -> None: """Calculate e, partial pressure of water vapor.""" svp = find_svp(self._t) # We have q = w/(w + 1), so w = q/(1 - q) @@ -379,23 +367,19 @@ def _find_e_from_q(self): self._e = w * self._R_v * (self._p - svp) / self._R_d - def _find_e_from_rh(self): + def _find_e_from_rh(self) -> None: """Calculate partial pressure of water vapor.""" svp = find_svp(self._t) self._e = self._rh / 100 * svp - def _get_wet_refractivity(self): - """ - Calculate the wet delay from pressure, temperature, and e - """ + def _get_wet_refractivity(self) -> None: + """Calculate the wet delay from pressure, temperature, and e.""" self._wet_refractivity = self._k2 * self._e / self._t + self._k3 * self._e / self._t**2 - def _get_hydro_refractivity(self): - """ - Calculate the hydrostatic delay from pressure and temperature - """ + def _get_hydro_refractivity(self) -> None: + """Calculate the hydrostatic delay from pressure and temperature.""" self._hydrostatic_refractivity = self._k1 * self._p / self._t @@ -407,7 +391,7 @@ def getHydroRefractivity(self): return self._hydrostatic_refractivity - def _adjust_grid(self, ll_bounds=None): + def _adjust_grid(self, ll_bounds=None) -> None: """ This function pads the weather grid with a level at self._zmin, if it does not already go that low. @@ -427,10 +411,10 @@ def _adjust_grid(self, ll_bounds=None): self._trimExtent(ll_bounds) - def _getZTD(self): + def _getZTD(self) -> None: """ Compute the full slant tropospheric delay for each weather model grid node, using the reference - height zref + height zref. """ wet = self.getWetRefractivity() hydro = self.getHydroRefractivity() @@ -449,9 +433,7 @@ def _getZTD(self): def _getExtent(self, lats, lons): - """ - get the bounding box around a set of lats/lons - """ + """Get the bounding box around a set of lats/lons.""" if (lats.size == 1) & (lons.size == 1): return [lats - self._lat_res, lats + self._lat_res, lons - self._lon_res, lons + self._lon_res] elif (lats.size > 1) & (lons.size > 1): @@ -511,7 +493,7 @@ def checkValidBounds( ): """ Checks whether the given bounding box is valid for the model - (i.e., intersects with the model domain at all) + (i.e., intersects with the model domain at all). Args: ll_bounds : np.ndarray @@ -586,24 +568,20 @@ def checkContainment(self: weatherModel, return weather_model_box.contains(input_box) - def _isOutside(self, extent1, extent2): + def _isOutside(self, extent1, extent2) -> bool: """ - Determine whether any of extent1 lies outside extent2 - extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon] + Determine whether any of extent1 lies outside extent2. + extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon]. """ t1 = extent1[0] < extent2[0] t2 = extent1[1] > extent2[1] t3 = extent1[2] < extent2[2] t4 = extent1[3] > extent2[3] - if np.any([t1, t2, t3, t4]): - return True - return False + return np.any([t1, t2, t3, t4]) - def _trimExtent(self, extent): - """ - get the bounding box around a set of lats/lons - """ + def _trimExtent(self, extent) -> None: + """Get the bounding box around a set of lats/lons.""" lat = self._lats.copy() lon = self._lons.copy() lat[np.isnan(lat)] = np.nanmean(lat) @@ -655,9 +633,7 @@ def _calculategeoh(self, z, lnsp): def getProjection(self): - """ - Returns: the native weather projection, which should be a pyproj object - """ + """Returns: the native weather projection, which should be a pyproj object.""" return self._proj @@ -665,10 +641,8 @@ def getPoints(self): return self._xs.copy(), self._ys.copy(), self._zs.copy() - def _uniform_in_z(self, _zlevels=None): - """ - Interpolate all variables to a regular grid in z - """ + def _uniform_in_z(self, _zlevels=None) -> None: + """Interpolate all variables to a regular grid in z.""" nx, ny = self._p.shape[:2] # new regular z-spacing @@ -696,10 +670,8 @@ def _uniform_in_z(self, _zlevels=None): self._ys = np.unique(self._ys) - def _checkForNans(self): - """ - Fill in NaN-values - """ + def _checkForNans(self) -> None: + """Fill in NaN-values.""" self._p = fillna3D(self._p) self._t = fillna3D(self._t, fill_value=1e16) # to avoid division by zero later on self._e = fillna3D(self._e) @@ -715,9 +687,7 @@ def out_file(self, outLoc): def filename(self, time=None, outLoc='weather_files'): - """ - Create a filename to store the weather model - """ + """Create a filename to store the weather model.""" os.makedirs(outLoc, exist_ok=True) if time is None: @@ -805,7 +775,7 @@ def write(self): return f -def make_weather_model_filename(name, time, ll_bounds): +def make_weather_model_filename(name, time, ll_bounds) -> str: s = np.floor(ll_bounds[0]) S = f'{np.abs(s):.0f}S' if s <0 else f'{s:.0f}N' @@ -821,7 +791,7 @@ def make_weather_model_filename(name, time, ll_bounds): def make_raw_weather_data_filename(outLoc, name, time): - """Filename generator for the raw downloaded weather model data""" + """Filename generator for the raw downloaded weather model data.""" f = os.path.join( outLoc, '{}_{}.{}'.format( @@ -834,9 +804,7 @@ def make_raw_weather_data_filename(outLoc, name, time): def find_svp(t): - """ - Calculate standard vapor presure. Should be model-specific - """ + """Calculate standard vapor presure. Should be model-specific.""" # From TRAIN: # Could not find the wrf used equation as they appear to be # mixed with latent heat etc. Istead I used the equations used @@ -869,7 +837,7 @@ def find_svp(t): def get_mapping(proj): - """Get CF-complient projection information from a proj""" + """Get CF-complient projection information from a proj.""" # In case of WGS-84 lat/lon, keep it simple if proj.to_epsg()==4326: return 'WGS84' @@ -882,7 +850,7 @@ def checkContainment_raw(path_wm_raw, buffer_deg: float = 1e-5) -> bool: """" Checks if existing raw weather model contains - requested ll_bounds + requested ll_bounds. Args: ---------- diff --git a/tools/RAiDER/models/wrf.py b/tools/RAiDER/models/wrf.py index 5d3070e5e..5fe81fbb1 100644 --- a/tools/RAiDER/models/wrf.py +++ b/tools/RAiDER/models/wrf.py @@ -15,12 +15,10 @@ # lats, lons = wrf.wm_nodes(*weather_files) # class WRF(WeatherModel): - """ - WRF class definition, based on the WeatherModel base class. - """ + """WRF class definition, based on the WeatherModel base class.""" # TODO: finish implementing - def __init__(self): + def __init__(self) -> None: WeatherModel.__init__(self) self._k1 = 0.776 # K/Pa @@ -33,13 +31,11 @@ def __init__(self): self._Name = 'WRF' self._time_res = TIME_RES[self._Name] - def _fetch(self): + def _fetch(self) -> None: pass - def load_weather(self, file1, file2, *args, **kwargs): - """ - Consistent class method to be implemented across all weather model types - """ + def load_weather(self, file1, file2, *args, **kwargs) -> None: + """Consistent class method to be implemented across all weather model types.""" try: lons, lats = self._get_wm_nodes(file1) self._read_netcdf(file2) @@ -85,10 +81,8 @@ def _get_wm_nodes(self, nodeFile): return lons, lats - def _read_netcdf(self, weatherFile, defNul=None): - """ - Read weather variables from a netCDF file - """ + def _read_netcdf(self, weatherFile, defNul=None) -> None: + """Read weather variables from a netCDF file.""" if defNul is None: defNul = np.nan @@ -162,29 +156,21 @@ def _read_netcdf(self, weatherFile, defNul=None): class UnitTypeError(Exception): - """ - Define a unit type exception for easily formatting - error messages for units - """ - + """Define a unit type exception for easily formatting error messages for units.""" def __init___(self, varName, unittype): msg = f"Unknown units for {varName}: '{unittype}'" Exception.__init__(self, msg) -def checkUnits(unitCheck, varName): - """ - Implement a check that the units are as expected - """ +def checkUnits(unitCheck, varName) -> None: + """Implement a check that the units are as expected.""" unitDict = {'pressure': 'Pa', 'temperature': 'K', 'relative humidity': '%', 'geopotential': 'm'} if unitCheck != unitDict[varName]: raise UnitTypeError(varName, unitCheck) def getNullValue(var): - """ - Get the null (or fill) value if it exists, otherwise set the null value to defNullValue - """ + """Get the null (or fill) value if it exists, otherwise set the null value to defNullValue.""" # NetCDF files have the ability to record their nodata value, but in the # particular NetCDF files that I'm reading, this field is left # unspecified and a nodata value of -999 is used. The solution I'm using diff --git a/tools/RAiDER/processWM.py b/tools/RAiDER/processWM.py index f3524f640..9474d920d 100755 --- a/tools/RAiDER/processWM.py +++ b/tools/RAiDER/processWM.py @@ -28,7 +28,7 @@ def prepareWeatherModel( makePlots: bool=False, force_download: bool=False, ) -> str: - """Parse inputs to download and prepare a weather model grid for interpolation + """Parse inputs to download and prepare a weather model grid for interpolation. Args: weather_model: WeatherModel - instantiated weather model object @@ -155,10 +155,8 @@ def _weather_model_debug( time, out, download_only -): - """ - raiderWeatherModelDebug main function. - """ +) -> None: + """RaiderWeatherModelDebug main function.""" logger.debug('Starting to run the weather model calculation with debugging plots') logger.debug('Time type: %s', type(time)) logger.debug('Time: %s', time.strftime('%Y%m%d')) diff --git a/tools/RAiDER/s1_azimuth_timing.py b/tools/RAiDER/s1_azimuth_timing.py index 4ba60af2f..368aa78f2 100644 --- a/tools/RAiDER/s1_azimuth_timing.py +++ b/tools/RAiDER/s1_azimuth_timing.py @@ -20,7 +20,8 @@ def _asf_query(point: Point, start: datetime.datetime, end: datetime.datetime, buffer_degrees: float = 2) -> list[str]: - """Using a buffer to get as many SLCs covering a given request as + """ + Using a buffer to get as many SLCs covering a given request as. Parameters ---------- @@ -48,7 +49,8 @@ def get_slc_id_from_point_and_time(lon: float, dt: datetime.datetime, buffer_seconds: int = 600, buffer_deg: float = 2) -> list: - """Obtains a (non-unique) SLC id from the lon/lat and datetime of inputs. The buffere ensures that + """ + Obtains a (non-unique) SLC id from the lon/lat and datetime of inputs. The buffere ensures that an SLC id is within the queried start/end times. Note an S1 scene takes roughly 30 seconds to acquire. Parameters @@ -196,7 +198,8 @@ def get_s1_azimuth_time_grid(lon: np.ndarray, def get_n_closest_datetimes(ref_time: datetime.datetime, n_target_times: int, time_step_hours: int) -> list[datetime.datetime]: - """Gets n closes times relative to the `round_to_hour_delta` and the + """ + Gets n closest times relative to the `round_to_hour_delta` and the `ref_time`. Specifically, if one is interetsted in getting 3 closest times to say 0, 6, 12, 18 UTC times of a ref time `dt`, then: ``` diff --git a/tools/RAiDER/s1_orbits.py b/tools/RAiDER/s1_orbits.py index cb506593e..6607e3f12 100644 --- a/tools/RAiDER/s1_orbits.py +++ b/tools/RAiDER/s1_orbits.py @@ -21,7 +21,7 @@ def _netrc_path() -> Path: def ensure_orbit_credentials() -> Optional[int]: - """Ensure credentials exist for ESA's CDSE and ASF's S1QC to download orbits + """Ensure credentials exist for ESA's CDSE and ASF's S1QC to download orbits. This method will prefer to use CDSE and NASA Earthdata credentials from your `~/.netrc` file if they exist, otherwise will look for environment variables and update or create your `~/.netrc` file. The environment variables @@ -65,7 +65,7 @@ def ensure_orbit_credentials() -> Optional[int]: def get_orbits_from_slc_ids(slc_ids: List[str], directory=Path.cwd()) -> List[Path]: - """Download all orbit files for a set of SLCs + """Download all orbit files for a set of SLCs. This method will ensure that the downloaded orbit files cover the entire acquisition start->stop time @@ -83,7 +83,7 @@ def get_orbits_from_slc_ids(slc_ids: List[str], directory=Path.cwd()) -> List[Pa def get_orbits_from_slc_ids_hyp3lib( slc_ids: list, orbit_directory: str = None ) -> dict: - """Reference: https://github.com/ACCESS-Cloud-Based-InSAR/DockerizedTopsApp/blob/dev/isce2_topsapp/localize_orbits.py#L23""" + """Reference: https://github.com/ACCESS-Cloud-Based-InSAR/DockerizedTopsApp/blob/dev/isce2_topsapp/localize_orbits.py#L23.""" # Populates env variables to netrc as required for sentineleof _ = ensure_orbit_credentials() esa_username, _, esa_password = netrc.netrc().authenticators(ESA_CDSE_HOST) @@ -106,7 +106,7 @@ def get_orbits_from_slc_ids_hyp3lib( def download_eofs(dts: list, missions: list, save_dir: str): - """Wrapper around sentineleof to first try downloading from ASF and fall back to CDSE""" + """Wrapper around sentineleof to first try downloading from ASF and fall back to CDSE.""" _ = ensure_orbit_credentials() orb_files = [] diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 12033a086..bdf6e13ed 100755 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -42,14 +42,14 @@ def projectDelays(delay, inc): - """Project zenith delays to LOS""" + """Project zenith delays to LOS.""" if inc==90: raise ZeroDivisionError return delay / cosd(inc) def floorish(val, frac): - """Round a value to the lower fractional part""" + """Round a value to the lower fractional part.""" return val - (val % frac) @@ -108,7 +108,7 @@ def enu2ecef( def ecef2enu(xyz, lat, lon, height): - """Convert ECEF xyz to ENU""" + """Convert ECEF xyz to ENU.""" x, y, z = xyz[..., 0], xyz[..., 1], xyz[..., 2] t = cosd(lon) * x + sind(lon) * y @@ -120,9 +120,7 @@ def ecef2enu(xyz, lat, lon, height): def rio_profile(fname): - """ - Reads the profile of a rasterio file - """ + """Reads the profile of a rasterio file.""" if rasterio is None: raise ImportError('RAiDER.utilFcns: rio_profile - rasterio is not installed') @@ -142,7 +140,7 @@ def rio_profile(fname): def rio_extents(profile): - """Get a bounding box in SNWE from a rasterio profile""" + """Get a bounding box in SNWE from a rasterio profile.""" gt = profile["transform"].to_gdal() xSize = profile["width"] ySize = profile["height"] @@ -152,9 +150,7 @@ def rio_extents(profile): def rio_open(fname, returnProj=False, userNDV=None, band=None): - """ - Reads a rasterio-compatible raster file and returns the data and profile - """ + """Reads a rasterio-compatible raster file and returns the data and profile.""" if rasterio is None: raise ImportError('RAiDER.utilFcns: rio_open - rasterio is not installed') @@ -196,10 +192,8 @@ def rio_open(fname, returnProj=False, userNDV=None, band=None): return data, profile -def nodataToNan(inarr, listofvals): - """ - Setting values to nan as needed - """ +def nodataToNan(inarr, listofvals) -> None: + """Setting values to nan as needed.""" inarr = inarr.astype(float) # nans cannot be integers (i.e. in DEM) for val in listofvals: if val is not None: @@ -239,9 +233,7 @@ def rio_stats(fname, band=1): def get_file_and_band(filestr): - """ - Support file;bandnum as input for filename strings - """ + """Support file;bandnum as input for filename strings.""" parts = filestr.split(";") # Defaults to first band if no bandnum is provided @@ -254,10 +246,8 @@ def get_file_and_band(filestr): f"Cannot interpret {filestr} as valid filename" ) -def writeArrayToRaster(array, filename, noDataValue=0., fmt='ENVI', proj=None, gt=None): - """ - write a numpy array to a GDAL-readable raster - """ +def writeArrayToRaster(array, filename, noDataValue=0., fmt='ENVI', proj=None, gt=None) -> None: + """Write a numpy array to a GDAL-readable raster.""" array_shp = np.shape(array) if array.ndim != 2: raise RuntimeError(f'writeArrayToRaster: cannot write an array of shape {array_shp} to a raster image') @@ -336,29 +326,23 @@ def _least_nonzero(a): def robmin(a): - """ - Get the minimum of an array, accounting for empty lists - """ + """Get the minimum of an array, accounting for empty lists.""" return np.nanmin(a) def robmax(a): - """ - Get the minimum of an array, accounting for empty lists - """ + """Get the minimum of an array, accounting for empty lists.""" return np.nanmax(a) def _get_g_ll(lats): - """ - Compute the variation in gravity constant with latitude - """ + """Compute the variation in gravity constant with latitude.""" return G1 * (1 - 0.002637 * cosd(2 * lats) + 0.0000059 * (cosd(2 * lats))**2) def get_Re(lats): """ - Returns earth radius as a function of latitude for WGS84 + Returns earth radius as a function of latitude for WGS84. Args: lats - ndarray of geodetic latitudes in degrees @@ -411,9 +395,7 @@ def geo_to_ht(lats, hts): def padLower(invar): - """ - add a layer of data below the lowest current z-level at height zmin - """ + """Add a layer of data below the lowest current z-level at height zmin.""" new_var = _least_nonzero(invar) return np.concatenate((new_var[:, :, np.newaxis], invar), axis=2) @@ -432,8 +414,8 @@ def round_time(dt, roundTo=60): def writeDelays(aoi, wetDelay, hydroDelay, wetFilename, hydroFilename=None, - outformat=None, ndv=0.): - """Write the delay numpy arrays to files in the format specified""" + outformat=None, ndv=0.) -> None: + """Write the delay numpy arrays to files in the format specified.""" if pd is None: raise ImportError('pandas is required to write GNSS delays to a file') @@ -473,9 +455,7 @@ def writeDelays(aoi, wetDelay, hydroDelay, def getTimeFromFile(filename): - """ - Parse a filename to get a date-time - """ + """Parse a filename to get a date-time.""" fmt = '%Y_%m_%d_T%H_%M_%S' p = re.compile(r'\d{4}_\d{2}_\d{2}_T\d{2}_\d{2}_\d{2}') out = p.search(filename).group() @@ -576,7 +556,7 @@ def UTM_to_WGS84(z, l, x, y): def transform_bbox(snwe_in, dest_crs=4326, src_crs=4326, margin=100.): """ - Transform bbox to lat/lon or another CRS for use with rest of workflow + Transform bbox to lat/lon or another CRS for use with rest of workflow. Returns: SNWE """ # TODO - Handle dateline crossing @@ -613,9 +593,7 @@ def transform_bbox(snwe_in, dest_crs=4326, src_crs=4326, margin=100.): def clip_bbox(bbox, spacing): - """ - Clip box to multiple of spacing - """ + """Clip box to multiple of spacing.""" return [np.floor(bbox[0] / spacing) * spacing, np.ceil(bbox[1] / spacing) * spacing, np.floor(bbox[2] / spacing) * spacing, @@ -637,8 +615,8 @@ def requests_retry_session(retries=10, session=None): return session -def writeWeatherVarsXarray(lat, lon, h, q, p, t, dt, crs, outName=None, NoDataValue=-9999, chunk=(1, 91, 144)): - +def writeWeatherVarsXarray(lat, lon, h, q, p, t, dt, crs, outName=None, NoDataValue=-9999, chunk=(1, 91, 144)) -> None: + # I added datetime as an input to the function and just copied these two lines from merra2 for the attrs_dict attrs_dict = { 'datetime': dt.strftime("%Y_%m_%dT%H_%M_%S"), @@ -687,7 +665,7 @@ def writeWeatherVarsXarray(lat, lon, h, q, p, t, dt, crs, outName=None, NoDataVa def convertLons(inLons): - """Convert lons from 0-360 to -180-180""" + """Convert lons from 0-360 to -180-180.""" mask = inLons > 180 outLons = inLons outLons[mask] = outLons[mask] - 360 @@ -718,8 +696,8 @@ def read_EarthData_loginInfo(filepath=None): return urs_usr, urs_pwd -def show_progress(block_num, block_size, total_size): - """Show download progress""" +def show_progress(block_num, block_size, total_size) -> None: + """Show download progress.""" if progressbar is None: raise ImportError('RAiDER.utilFcns: show_progress - progressbar is not available') @@ -737,7 +715,7 @@ def show_progress(block_num, block_size, total_size): def getChunkSize(in_shape): - """Create a reasonable chunk size""" + """Create a reasonable chunk size.""" if mp is None: raise ImportError('RAiDER.utilFcns: getChunkSize - multiprocessing is not available') minChunkSize = 100 @@ -837,7 +815,7 @@ def calcgeoh(lnsp, t, q, z, a, b, R_d, num_levels): def transform_coords(proj1, proj2, x, y): """ Transform coordinates from proj1 to proj2 (can be EPSG or crs from proj). - e.g. x, y = transform_coords(4326, 4087, lon, lat) + e.g. x, y = transform_coords(4326, 4087, lon, lat). """ transformer = Transformer.from_crs(proj1, proj2, always_xy=True) return transformer.transform(x, y) @@ -845,7 +823,7 @@ def transform_coords(proj1, proj2, x, y): def get_nearest_wmtimes(t0, time_delta): """" - Get the nearest two available times to the requested time given a time step + Get the nearest two available times to the requested time given a time step. Args: t0 - user-requested Python datetime @@ -883,7 +861,7 @@ def get_nearest_wmtimes(t0, time_delta): def get_dt(t1,t2): """ Helper function for getting the absolute difference in seconds between - two python datetimes + two python datetimes. Args: t1, t2 - Python datetimes From 5e4efae932a74184eb10160979e648d89c44da6c Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:52:44 -0500 Subject: [PATCH 09/76] ruff format - Use double quotes instead of single quotes, except where using double quotes would prevent you from having to escape an apostrophe - Removed unnecessary parentheses - Exactly 2 spaces before a same-line comment - Function calls formatted - Exactly 1 space on either side of an operator (=, +, -, <, etc.) - 1 line between top-of-file docstring and imports - Space before argument type annotation - Space between equals sign in deafult argument - Trailing comma - Exactly 1 empty line at the end of a file - No empty lines at the beginning of a file - Use ".0" instead of "." to denote float literals - One space after a comma - One # for a comment - No extraneous returns --- test/credentials/test_envVars.py | 5 +- test/credentials/test_updateTrue.py | 5 +- tools/RAiDER/__init__.py | 1 + tools/RAiDER/aria/calcGUNW.py | 85 +- tools/RAiDER/aria/prepFromGUNW.py | 147 +- tools/RAiDER/aws.py | 14 +- tools/RAiDER/checkArgs.py | 35 +- tools/RAiDER/cli/__init__.py | 1 + tools/RAiDER/cli/__main__.py | 14 +- tools/RAiDER/cli/conf.py | 3 +- tools/RAiDER/cli/parser.py | 16 +- tools/RAiDER/cli/raider.py | 363 +-- tools/RAiDER/cli/statsPlot.py | 2730 +++++++++++++++++------ tools/RAiDER/cli/validators.py | 125 +- tools/RAiDER/constants.py | 12 +- tools/RAiDER/delay.py | 204 +- tools/RAiDER/delayFcns.py | 18 +- tools/RAiDER/dem.py | 26 +- tools/RAiDER/getStationDelays.py | 92 +- tools/RAiDER/gnss/downloadGNSSDelays.py | 85 +- tools/RAiDER/gnss/processDelayFiles.py | 197 +- tools/RAiDER/interpolator.py | 31 +- tools/RAiDER/llreader.py | 117 +- tools/RAiDER/logger.py | 49 +- tools/RAiDER/losreader.py | 138 +- tools/RAiDER/models/credentials.py | 34 +- tools/RAiDER/models/customExceptions.py | 22 +- tools/RAiDER/models/ecmwf.py | 180 +- tools/RAiDER/models/era5.py | 8 +- tools/RAiDER/models/era5t.py | 10 +- tools/RAiDER/models/erai.py | 2 +- tools/RAiDER/models/generateGACOSVRT.py | 38 +- tools/RAiDER/models/gmao.py | 72 +- tools/RAiDER/models/hres.py | 25 +- tools/RAiDER/models/hrrr.py | 110 +- tools/RAiDER/models/merra2.py | 48 +- tools/RAiDER/models/model_levels.py | 30 +- tools/RAiDER/models/ncmr.py | 115 +- tools/RAiDER/models/plotWeather.py | 50 +- tools/RAiDER/models/template.py | 18 +- tools/RAiDER/models/weatherModel.py | 268 +-- tools/RAiDER/models/wrf.py | 27 +- tools/RAiDER/processWM.py | 77 +- tools/RAiDER/s1_azimuth_timing.py | 145 +- tools/RAiDER/s1_orbits.py | 20 +- tools/RAiDER/utilFcns.py | 178 +- 46 files changed, 3588 insertions(+), 2402 deletions(-) diff --git a/test/credentials/test_envVars.py b/test/credentials/test_envVars.py index 9c8bcd071..9658e22fb 100644 --- a/test/credentials/test_envVars.py +++ b/test/credentials/test_envVars.py @@ -102,7 +102,4 @@ def test_envVars( actual_content = rc_path.read_text() rc_path.unlink() - assert ( - expected_content == actual_content, - f'{rc_path} was not updated correctly' - ) + assert expected_content == actual_content, f'{rc_path} was not updated correctly' diff --git a/test/credentials/test_updateTrue.py b/test/credentials/test_updateTrue.py index 9ed65fbc2..c4265c6d6 100644 --- a/test/credentials/test_updateTrue.py +++ b/test/credentials/test_updateTrue.py @@ -85,7 +85,4 @@ def test_updateTrue(model_name, template): actual_content = rc_path.read_text() rc_path.unlink() - assert ( - expected_content == actual_content, - f'{rc_path} was not updated correctly' - ) + assert expected_content == actual_content, f'{rc_path} was not updated correctly' diff --git a/tools/RAiDER/__init__.py b/tools/RAiDER/__init__.py index 81ece0dba..f736f7b8e 100644 --- a/tools/RAiDER/__init__.py +++ b/tools/RAiDER/__init__.py @@ -3,6 +3,7 @@ Copyright (c) 2019-2022, California Institute of Technology ("Caltech"). All rights reserved. """ + from importlib.metadata import version diff --git a/tools/RAiDER/aria/calcGUNW.py b/tools/RAiDER/aria/calcGUNW.py index 65e2a43ee..627528f56 100644 --- a/tools/RAiDER/aria/calcGUNW.py +++ b/tools/RAiDER/aria/calcGUNW.py @@ -2,6 +2,7 @@ Calculate the interferometric phase from the 4 delays files of a GUNW and write it to disk. """ + import os from datetime import datetime @@ -14,12 +15,12 @@ from RAiDER.logger import logger -## ToDo: - # Check difference direction +# ToDo: +# Check difference direction TROPO_GROUP = 'science/grids/corrections/external/troposphere' TROPO_NAMES = ['troposphereWet', 'troposphereHydrostatic'] -DIM_NAMES = ['heightsMeta', 'latitudeMeta', 'longitudeMeta'] +DIM_NAMES = ['heightsMeta', 'latitudeMeta', 'longitudeMeta'] def compute_delays_slc(cube_filenames: list, wavelength: float) -> xr.Dataset: @@ -61,13 +62,13 @@ def compute_delays_slc(cube_filenames: list, wavelength: float) -> xr.Dataset: hyd_delays.append(da_hydro) attrs_lst.append(ds.attrs) - chunk_sizes = da_wet.shape[0], da_wet.shape[1]/3, da_wet.shape[2]/3 + chunk_sizes = da_wet.shape[0], da_wet.shape[1] / 3, da_wet.shape[2] / 3 # open one to copy and store new data ds_slc = xr.open_dataset(path).copy() encoding = ds_slc['wet'].encoding # chunksizes and fill value encoding['contiguous'] = False - encoding['_FillValue'] = 0. + encoding['_FillValue'] = 0.0 encoding['chunksizes'] = tuple([np.floor(cs) for cs in chunk_sizes]) del ds_slc['wet'], ds_slc['hydro'] @@ -78,28 +79,27 @@ def compute_delays_slc(cube_filenames: list, wavelength: float) -> xr.Dataset: model = os.path.basename(path).split('_')[0] attrs = { - 'units': 'radians', - 'grid_mapping': 'crs', - } + 'units': 'radians', + 'grid_mapping': 'crs', + } - ## no data (fill value?) chunk size? + # no data (fill value?) chunk size? for name in TROPO_NAMES: for k, key in enumerate(['reference', 'secondary']): descrip = f"Delay due to {name.lstrip('troposphere')} component of troposphere" - da_attrs = {**attrs, - 'description': descrip, - 'long_name': name, - 'standard_name': name, - 'RAiDER version': RAiDER.__version__, - 'model_times_used': attrs_lst[k]['model_times_used'], - 'scene_center_time': attrs_lst[k]['reference_time'], - 'time_interpolation_method': attrs_lst[k]['interpolation_method'] - } + da_attrs = { + **attrs, + 'description': descrip, + 'long_name': name, + 'standard_name': name, + 'RAiDER version': RAiDER.__version__, + 'model_times_used': attrs_lst[k]['model_times_used'], + 'scene_center_time': attrs_lst[k]['reference_time'], + 'time_interpolation_method': attrs_lst[k]['interpolation_method'], + } ds_slc[f'{key}_{name}'] = ds_slc[f'{key}_{name}'].assign_attrs(da_attrs) ds_slc[f'{key}_{name}'].encoding = encoding - ds_slc = ds_slc.assign_attrs(model=model, - method='ray tracing' - ) + ds_slc = ds_slc.assign_attrs(model=model, method='ray tracing') # force these to float32 to prevent stitching errors coords = {coord: ds_slc[coord].astype(np.float32) for coord in ds_slc.coords} @@ -107,9 +107,10 @@ def compute_delays_slc(cube_filenames: list, wavelength: float) -> xr.Dataset: return ds_slc.rename(z=DIM_NAMES[0], y=DIM_NAMES[1], x=DIM_NAMES[2]) + # first need to delete the variable; only can seem to with h5 + - ## first need to delete the variable; only can seem to with h5 -def update_gunw_slc(path_gunw:str, ds_slc) -> None: +def update_gunw_slc(path_gunw: str, ds_slc) -> None: """Update the path_gunw file using the slc delays in ds_slc.""" with h5py.File(path_gunw, 'a') as h5: for k in TROPO_GROUP.split(): @@ -125,69 +126,61 @@ def update_gunw_slc(path_gunw:str, ds_slc) -> None: if k in h5.keys(): del h5[k] - with Dataset(path_gunw, mode='a') as ds: - ds_grp = ds[TROPO_GROUP] + ds_grp = ds[TROPO_GROUP] ds_grp.createGroup(ds_slc.attrs['model'].upper()) ds_grp_wm = ds_grp[ds_slc.attrs['model'].upper()] - - ## create and store new data e.g., corrections/troposphere/GMAO/reference/troposphereWet + # create and store new data e.g., corrections/troposphere/GMAO/reference/troposphereWet for rs in 'reference secondary'.split(): ds_grp_wm.createGroup(rs) ds_grp_rs = ds_grp_wm[rs] - ## create the new dimensions e.g., corrections/troposphere/GMAO/reference/latitudeMeta + # create the new dimensions e.g., corrections/troposphere/GMAO/reference/latitudeMeta for dim in DIM_NAMES: - ## dimension may already exist if updating + # dimension may already exist if updating try: ds_grp_rs.createDimension(dim, len(ds_slc.coords[dim])) - ## necessary for transform - v = ds_grp_rs.createVariable(dim, np.float32, dim) + # necessary for transform + v = ds_grp_rs.createVariable(dim, np.float32, dim) v[:] = ds_slc[dim] v.setncatts(ds_slc[dim].attrs) except RuntimeError: pass - ## add the projection if it doesnt exist + # add the projection if it doesnt exist try: v_proj = ds_grp_rs.createVariable('crs', 'i') except RuntimeError: v_proj = ds_grp_rs['crs'] - v_proj.setncatts(ds_slc["crs"].attrs) + v_proj.setncatts(ds_slc['crs'].attrs) - ## update the actual tropo data + # update the actual tropo data for name in TROPO_NAMES: - da = ds_slc[f'{rs}_{name}'] - nodata = da.encoding['_FillValue'] + da = ds_slc[f'{rs}_{name}'] + nodata = da.encoding['_FillValue'] chunksize = da.encoding['chunksizes'] - ## in case updating + # in case updating try: - v = ds_grp_rs.createVariable(name, np.float32, DIM_NAMES, - chunksizes=chunksize, fill_value=nodata) + v = ds_grp_rs.createVariable(name, np.float32, DIM_NAMES, chunksizes=chunksize, fill_value=nodata) except RuntimeError: - v = ds_grp_rs[name] + v = ds_grp_rs[name] v[:] = da.data v.setncatts(da.attrs) - logger.info('Updated %s group in: %s', os.path.basename(TROPO_GROUP), path_gunw) - return def update_gunw_version(path_gunw) -> None: """Temporary hack for updating version to test aria-tools.""" with Dataset(path_gunw, mode='a') as ds: ds.version = '1c' - return -def tropo_gunw_slc(cube_filenames: list, - path_gunw: str, - wavelength: float) -> xr.Dataset: +def tropo_gunw_slc(cube_filenames: list, path_gunw: str, wavelength: float) -> xr.Dataset: """ Compute and format the troposphere phase delay for GUNW from RAiDER outputs. diff --git a/tools/RAiDER/aria/prepFromGUNW.py b/tools/RAiDER/aria/prepFromGUNW.py index 336d3dcea..6021816d6 100644 --- a/tools/RAiDER/aria/prepFromGUNW.py +++ b/tools/RAiDER/aria/prepFromGUNW.py @@ -27,7 +27,7 @@ from RAiDER.s1_orbits import get_orbits_from_slc_ids_hyp3lib -## cube spacing in degrees for each model +# cube spacing in degrees for each model DCT_POSTING = {'HRRR': 0.05, 'HRES': 0.10, 'GMAO': 0.10, 'ERA5': 0.10, 'ERA5T': 0.10, 'MERRA2': 0.1} @@ -39,16 +39,17 @@ def _get_acq_time_from_gunw_id(gunw_id: str, reference_or_secondary: str) -> dat date_tokens = tokens[6].split('_') date_token = date_tokens[0] if reference_or_secondary == 'reference' else date_tokens[1] center_time_token = tokens[7] - cen_acq_time = datetime(int(date_token[:4]), - int(date_token[4:6]), - int(date_token[6:]), - int(center_time_token[:2]), - int(center_time_token[2:4]), - int(center_time_token[4:])) + cen_acq_time = datetime( + int(date_token[:4]), + int(date_token[4:6]), + int(date_token[6:]), + int(center_time_token[:2]), + int(center_time_token[2:4]), + int(center_time_token[4:]), + ) return cen_acq_time - def check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id: str) -> bool: """ Determine if all the times for azimuth interpolation are available using @@ -79,8 +80,7 @@ def check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id: st return all(ref_dataset_availability) and all(sec_dataset_availability) -def get_slc_ids_from_gunw(gunw_path: str, - reference_or_secondary: str = 'reference') -> list[str]: +def get_slc_ids_from_gunw(gunw_path: str, reference_or_secondary: str = 'reference') -> list[str]: # Example input: test/gunw_test_data/S1-GUNW-D-R-059-tops-20230320_20220418-180300-00179W_00051N-PP-c92e-v2_0_6.nc if reference_or_secondary not in ['reference', 'secondary']: raise ValueError('"reference_or_secondary" must be either "reference" or "secondary"') @@ -96,9 +96,7 @@ def get_acq_time_from_slc_id(slc_id: str) -> pd.Timestamp: return pd.Timestamp(ts_str) - -def check_weather_model_availability(gunw_path: str, - weather_model_name: str) -> bool: +def check_weather_model_availability(gunw_path: str, weather_model_name: str) -> bool: """ Check weather reference and secondary dates of GUNW occur within weather model valid range. @@ -152,7 +150,7 @@ def check_weather_model_availability(gunw_path: str, wm_start_date, wm_end_date = weather_model._valid_range if not isinstance(wm_end_date, datetime): - raise ValueError(f'the weather model\'s end date is not valid: {wm_end_date}') + raise ValueError(f"the weather model's end date is not valid: {wm_end_date}") ref_cond = ref_ts <= wm_end_date sec_cond = sec_ts >= wm_start_date return ref_cond and sec_cond @@ -165,64 +163,60 @@ class GUNW: out_dir: str def __post_init__(self): - self.SNWE = self.get_bbox() - self.heights = np.arange(-500, 9500, 500).tolist() + self.SNWE = self.get_bbox() + self.heights = np.arange(-500, 9500, 500).tolist() # self.heights = [-500, 0] self.dates, self.mid_time = self.get_datetimes() - self.look_dir = self.get_look_dir() + self.look_dir = self.get_look_dir() self.wavelength = self.get_wavelength() - self.name = self.make_fname() - self.OrbitFile = self.get_orbit_file() - self.spacing_m = int(DCT_POSTING[self.wm] * 1e5) + self.name = self.make_fname() + self.OrbitFile = self.get_orbit_file() + self.spacing_m = int(DCT_POSTING[self.wm] * 1e5) - ## not implemented + # not implemented # self.spacing_m = self.calc_spacing_UTM() # probably wrong/unnecessary # self.lat_file, self.lon_file = self.makeLatLonGrid_native() # self.path_cube = self.make_cube() # not needed - def get_bbox(self): """Get the bounding box (SNWE) from an ARIA GUNW product.""" with xr.open_dataset(self.path_gunw) as ds: poly_str = ds['productBoundingBox'].data[0].decode('utf-8') - poly = shapely.wkt.loads(poly_str) + poly = shapely.wkt.loads(poly_str) W, S, E, N = poly.bounds return [S, N, W, E] - def make_fname(self) -> str: """Match the ref/sec filename (SLC dates may be different around edge cases).""" ref, sec = os.path.basename(self.path_gunw).split('-')[6].split('_') mid_time = os.path.basename(self.path_gunw).split('-')[7] return f'{ref}-{sec}_{mid_time}' - def get_datetimes(self): """Get the datetimes and set the satellite for orbit.""" - ref_sec = self.get_slc_dt() + ref_sec = self.get_slc_dt() middates = [] for aq in ref_sec: - st, en = aq - midpt = st + (en-st)/2 + st, en = aq + midpt = st + (en - st) / 2 middates.append(int(midpt.date().strftime('%Y%m%d'))) midtime = midpt.time().strftime('%H:%M:%S') return middates, midtime - def get_slc_dt(self): """Grab the SLC start date and time from the GUNW.""" - group = 'science/radarMetaData/inputSLC' + group = 'science/radarMetaData/inputSLC' lst_sten = [] - for i, key in enumerate('reference secondary'.split()): + for key in 'reference secondary'.split(): ds = xr.open_dataset(self.path_gunw, group=f'{group}/{key}') slcs = ds['L1InputGranules'] nslcs = slcs.count().item() # single slc if nslcs == 1: - slc = slcs.item() + slc = slcs.item() assert slc, f'Missing {key} SLC metadata in GUNW: {self.f}' st = datetime.strptime(slc.split('_')[5], '%Y%m%dT%H%M%S') en = datetime.strptime(slc.split('_')[6], '%Y%m%dT%H%M%S') @@ -231,48 +225,45 @@ def get_slc_dt(self): for j in range(nslcs): slc = slcs.data[j] if slc: - ## get the maximum range + # get the maximum range st_tmp = datetime.strptime(slc.split('_')[5], '%Y%m%dT%H%M%S') en_tmp = datetime.strptime(slc.split('_')[6], '%Y%m%dT%H%M%S') - ## check the second SLC is within one day of the previous + # check the second SLC is within one day of the previous if st > datetime(1989, 3, 1): stdiff = np.abs((st_tmp - st).days) endiff = np.abs((en_tmp - en).days) assert stdiff < 2 and endiff < 2, 'SLCs granules are too far apart in time. Incorrect metadata' - st = st_tmp if st_tmp > st else st en = en_tmp if en_tmp > en else en - assert st>datetime(1989, 3, 1), f'Missing {key} SLC metadata in GUNW: {self.f}' + assert st > datetime(1989, 3, 1), \ + f'Missing {key} SLC metadata in GUNW: {self.f}' lst_sten.append([st, en]) return lst_sten - def get_look_dir(self) -> Literal['right', 'left']: look_dir = os.path.basename(self.path_gunw).split('-')[3].lower() return 'right' if look_dir == 'r' else 'left' - def get_wavelength(self): - group ='science/radarMetaData' + group = 'science/radarMetaData' with xr.open_dataset(self.path_gunw, group=group) as ds: wavelength = ds['wavelength'].item() return wavelength - def get_orbit_file(self): """Get orbit file for reference (GUNW: first & later date).""" orbit_dir = os.path.join(self.out_dir, 'orbits') os.makedirs(orbit_dir, exist_ok=True) # just to get the correct satellite - group = 'science/radarMetaData/inputSLC/reference' + group = 'science/radarMetaData/inputSLC/reference' - ds = xr.open_dataset(self.path_gunw, group=f'{group}') + ds = xr.open_dataset(self.path_gunw, group=f'{group}') slcs = ds['L1InputGranules'] # Convert to list of strings slcs_lst = [slc for slc in slcs.data.tolist() if slc] @@ -283,42 +274,38 @@ def get_orbit_file(self): return [str(o) for o in path_orb] - - ## ------ methods below are not used + # ------ methods below are not used def get_version(self): with xr.open_dataset(self.path_gunw) as ds: version = ds.attrs['version'] return version - def getHeights(self): """Get the 4 height levels within a GUNW.""" - group ='science/grids/imagingGeometry' + group = 'science/grids/imagingGeometry' with xr.open_dataset(self.path_gunw, group=group) as ds: hgts = ds.heightsMeta.data.tolist() return hgts - - def calc_spacing_UTM(self, posting:float=0.01): + def calc_spacing_UTM(self, posting: float = 0.01): """Convert desired horizontal posting in degrees to meters. Want to calculate delays close to native model resolution (3 km for HRR) """ from RAiDER.utilFcns import WGS84_to_UTM + group = 'science/grids/data' with xr.open_dataset(self.path_gunw, group=group) as ds0: lats = ds0.latitude.data lons = ds0.longitude.data - lat0, lon0 = lats[0], lons[0] lat1, lon1 = lat0 + posting, lon0 + posting - res = WGS84_to_UTM(np.array([lon0, lon1]), np.array([lat0, lat1])) + res = WGS84_to_UTM(np.array([lon0, lon1]), np.array([lat0, lat1])) lon_spacing_m = np.subtract(*res[2][::-1]) lat_spacing_m = np.subtract(*res[3][::-1]) return np.mean([lon_spacing_m, lat_spacing_m]) - def makeLatLonGrid_native(self): """Make LatLonGrid at GUNW spacing (90m = 0.00083333º).""" group = 'science/grids/data' @@ -326,9 +313,9 @@ def makeLatLonGrid_native(self): lats = ds0.latitude.data lons = ds0.longitude.data - Lat, Lon = np.meshgrid(lats, lons) + Lat, Lon = np.meshgrid(lats, lons) - dims = 'longitude latitude'.split() + dims = 'longitude latitude'.split() da_lon = xr.DataArray(Lon.T, coords=[Lon[0, :], Lat[:, 0]], dims=dims) da_lat = xr.DataArray(Lat.T, coords=[Lon[0, :], Lat[:, 0]], dims=dims) @@ -342,7 +329,6 @@ def makeLatLonGrid_native(self): logger.debug('Wrote: %s', dst_lon) return dst_lat, dst_lon - def make_cube(self): """Make LatLonGrid at GUNW spacing (90m = 0.00083333º).""" group = 'science/grids/data' @@ -364,19 +350,13 @@ def make_cube(self): return dst_cube -def update_yaml(dct_cfg:dict, dst:str='GUNW.yaml'): +def update_yaml(dct_cfg: dict, dst: str = 'GUNW.yaml'): """Write a new yaml file from a dictionary. Updates parameters in the default 'template.yaml' file. Each key:value pair will in 'dct_cfg' will overwrite that in the default """ - run_config_path = os.path.join( - os.path.dirname(RAiDER.__file__), - 'cli', - 'examples', - 'template', - 'template.yaml' - ) + run_config_path = os.path.join(os.path.dirname(RAiDER.__file__), 'cli', 'examples', 'template', 'template.yaml') with open(run_config_path) as f: try: @@ -388,9 +368,9 @@ def update_yaml(dct_cfg:dict, dst:str='GUNW.yaml'): params = {**params, **dct_cfg} with open(dst, 'w') as fh: - yaml.safe_dump(params, fh, default_flow_style=False) + yaml.safe_dump(params, fh, default_flow_style=False) - logger.info ('Wrote new cfg file: %s', dst) + logger.info('Wrote new cfg file: %s', dst) return dst @@ -401,24 +381,27 @@ def main(args): GUNWObj = GUNW(args.file, args.weather_model, args.output_directory) - raider_cfg = { - 'weather_model': args.weather_model, - 'look_dir': GUNWObj.look_dir, - 'cube_spacing_in_m': GUNWObj.spacing_m, - 'aoi_group' : {'bounding_box': GUNWObj.SNWE}, - 'height_group' : {'height_levels': GUNWObj.heights}, - 'date_group': {'date_list': GUNWObj.dates}, - 'time_group': {'time': GUNWObj.mid_time, - # Options are 'none', 'center_time', and 'azimuth_time_grid' - 'interpolate_time': args.interpolate_time}, - 'los_group' : {'ray_trace': True, - 'orbit_file': GUNWObj.OrbitFile, - 'wavelength': GUNWObj.wavelength, - }, - - 'runtime_group': {'raster_format': 'nc', - 'output_directory': args.output_directory, - } + raider_cfg = { + 'weather_model': args.weather_model, + 'look_dir': GUNWObj.look_dir, + 'cube_spacing_in_m': GUNWObj.spacing_m, + 'aoi_group': {'bounding_box': GUNWObj.SNWE}, + 'height_group': {'height_levels': GUNWObj.heights}, + 'date_group': {'date_list': GUNWObj.dates}, + 'time_group': { + 'time': GUNWObj.mid_time, + # Options are 'none', 'center_time', and 'azimuth_time_grid' + 'interpolate_time': args.interpolate_time, + }, + 'los_group': { + 'ray_trace': True, + 'orbit_file': GUNWObj.OrbitFile, + 'wavelength': GUNWObj.wavelength, + }, + 'runtime_group': { + 'raster_format': 'nc', + 'output_directory': args.output_directory, + }, } path_cfg = f'GUNW_{GUNWObj.name}.yaml' diff --git a/tools/RAiDER/aws.py b/tools/RAiDER/aws.py index 23b2334ef..c0d035601 100644 --- a/tools/RAiDER/aws.py +++ b/tools/RAiDER/aws.py @@ -11,14 +11,7 @@ def get_tag_set(): - tag_set = { - 'TagSet': [ - { - 'Key': 'file_type', - 'Value': 'product' - } - ] - } + tag_set = {'TagSet': [{'Key': 'file_type', 'Value': 'product'}]} return tag_set @@ -43,10 +36,7 @@ def upload_file_to_s3(path_to_file: Union[str, Path], bucket: str, prefix: str = def get_s3_file(bucket_name: str, bucket_prefix: str, file_type: str) -> Optional[str]: - result = S3_CLIENT.list_objects_v2( - Bucket=bucket_name, - Prefix=bucket_prefix - ) + result = S3_CLIENT.list_objects_v2(Bucket=bucket_name, Prefix=bucket_prefix) for s3_object in result['Contents']: key = s3_object['Key'] if key.endswith(file_type): diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index 0c35b729f..c7784730e 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -32,10 +32,10 @@ def checkArgs(args): # Date and Time parsing args.date_list = [datetime.combine(d, args.time) for d in args.date_list] if (len(args.date_list) > 1) & (args.orbit_file is not None): - logger.warning('Only one orbit file is being used to get the ' - 'look vectors for all requested times, if you ' - 'want to use separate orbit files you will ' - 'need to run raider separately for each time.') + logger.warning( + 'Only one orbit file is being used to get the look vectors for all requested times, if you want to use ' + 'separate orbit files you will need to run raider separately for each time.' + ) args.los.setTime(args.date_list[0]) @@ -43,20 +43,18 @@ def checkArgs(args): # filenames wetNames, hydroNames = [], [] for d in args.date_list: - if (args.aoi.type() != 'bounding_box'): - + if args.aoi.type() != 'bounding_box': # Handle the GNSS station file - if (args.aoi.type()=='station_file'): + if args.aoi.type() == 'station_file': wetFilename = os.path.join( args.output_directory, - f'{args.weather_model._dataset.upper()}_Delay'\ - f'_{d.strftime("%Y%m%dT%H%M%S")}_ztd.csv' + f'{args.weather_model._dataset.upper()}_Delay' f'_{d.strftime("%Y%m%dT%H%M%S")}_ztd.csv', ) - hydroFilename = '' # only the 'wetFilename' is used for the station_file + hydroFilename = '' # only the 'wetFilename' is used for the station_file # copy the input station file to the output location for editing - indf = pd.read_csv(args.aoi._filename).drop_duplicates(subset=["Lat", "Lon"]) + indf = pd.read_csv(args.aoi._filename).drop_duplicates(subset=['Lat', 'Lon']) indf.to_csv(wetFilename, index=False) else: @@ -70,7 +68,6 @@ def checkArgs(args): args.output_directory, ) - else: # In this case a cube file format is needed if args.file_format not in '.nc .h5 h5 hdf5 .hdf5 nc'.split(): @@ -98,7 +95,7 @@ def checkArgs(args): def get_raster_ext(fmt): drivers = rd.raster_driver_extensions() - extensions = {value.upper():key for key, value in drivers.items()} + extensions = {value.upper(): key for key, value in drivers.items()} # add in ENVI/ISCE formats with generic extension extensions['ENVI'] = '.dat' @@ -120,15 +117,13 @@ def makeDelayFileNames(time, los, outformat, weather_model_name, out): >>> makeDelayFileNames(None, None, "h5", "model_name", "some_dir") ('some_dir/model_name_wet_ztd.h5', 'some_dir/model_name_hydro_ztd.h5') """ - format_string = "{model_name}_{{}}_{time}{los}.{ext}".format( + format_string = '{model_name}_{{}}_{time}{los}.{ext}'.format( model_name=weather_model_name, - time=time.strftime("%Y%m%dT%H%M%S_") if time is not None else "", - los="ztd" if (isinstance(los, Zenith) or los is None) else "std", - ext=outformat - ) - hydroname, wetname = ( - format_string.format(dtyp) for dtyp in ('hydro', 'wet') + time=time.strftime('%Y%m%dT%H%M%S_') if time is not None else '', + los='ztd' if (isinstance(los, Zenith) or los is None) else 'std', + ext=outformat, ) + hydroname, wetname = (format_string.format(dtyp) for dtyp in ('hydro', 'wet')) hydro_file_name = os.path.join(out, hydroname) wet_file_name = os.path.join(out, wetname) diff --git a/tools/RAiDER/cli/__init__.py b/tools/RAiDER/cli/__init__.py index 1cab33255..b1dd22c39 100644 --- a/tools/RAiDER/cli/__init__.py +++ b/tools/RAiDER/cli/__init__.py @@ -6,6 +6,7 @@ class AttributeDict(dict): __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ + DEFAULT_DICT = AttributeDict( dict( look_dir='right', diff --git a/tools/RAiDER/cli/__main__.py b/tools/RAiDER/cli/__main__.py index a716faf07..7756cf424 100644 --- a/tools/RAiDER/cli/__main__.py +++ b/tools/RAiDER/cli/__main__.py @@ -6,15 +6,12 @@ def main() -> None: - parser = argparse.ArgumentParser( - prefix_chars='+', - formatter_class=argparse.ArgumentDefaultsHelpFormatter - ) + parser = argparse.ArgumentParser(prefix_chars='+', formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument( '++process', choices=['calcDelays', 'downloadGNSS', 'calcDelaysGUNW'], default='calcDelays', - help='Select the entrypoint to use' + help='Select the entrypoint to use', ) parser.add_argument( '++logger_path', @@ -32,14 +29,11 @@ def main() -> None: try: # python >=3.10 interface - (process_entry_point,) = entry_points( - group='console_scripts', name=f'{args.process}.py') + (process_entry_point,) = entry_points(group='console_scripts', name=f'{args.process}.py') except TypeError: # python 3.8 and 3.9 interface scripts = entry_points()['console_scripts'] - process_entry_point = [ - ep for ep in scripts if ep.name == f'{args.process}.py' - ][0] + process_entry_point = [ep for ep in scripts if ep.name == f'{args.process}.py'][0] process_entry_point.load()() diff --git a/tools/RAiDER/cli/conf.py b/tools/RAiDER/cli/conf.py index 53a70c9ca..73d5e7356 100644 --- a/tools/RAiDER/cli/conf.py +++ b/tools/RAiDER/cli/conf.py @@ -1,7 +1,6 @@ - LOGGER_PATH = None + def setLoggerPath(path) -> None: global LOGGER_PATH LOGGER_PATH = path - diff --git a/tools/RAiDER/cli/parser.py b/tools/RAiDER/cli/parser.py index 5e6a97b20..d7aa9f2e7 100644 --- a/tools/RAiDER/cli/parser.py +++ b/tools/RAiDER/cli/parser.py @@ -7,8 +7,7 @@ def add_cpus(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--cpus', - help='The number of cpus to be used for multiprocessing or "all" for ' - 'all available cpus.', + help='The number of cpus to be used for multiprocessing or "all" for all available cpus.', type=IntegerMappingType(0, all=os.cpu_count()), default='all', ) @@ -24,20 +23,17 @@ def add_verbose(parser: argparse.ArgumentParser) -> None: def add_out(parser: argparse.ArgumentParser) -> None: - parser.add_argument( - '--out', - help='Output directory', - default='.' - ) + parser.add_argument('--out', help='Output directory', default='.') def add_bbox(parser: argparse.ArgumentParser) -> None: parser.add_argument( - '--bbox', '-b', - help="Bounding box", + '--bbox', + '-b', + help='Bounding box', nargs=4, type=float, dest='query_area', action=BBoxAction, - metavar=('S', 'N', 'W', 'E') + metavar=('S', 'N', 'W', 'E'), ) diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index ef81e087d..4f29b2925 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -44,7 +44,7 @@ raider.py run_config_file.yaml """ -DEFAULT_RUN_CONFIG_PATH = os.path.abspath("./raider.yaml") +DEFAULT_RUN_CONFIG_PATH = os.path.abspath('./raider.yaml') def read_run_config_file(fname): @@ -61,20 +61,19 @@ def read_run_config_file(fname): """ from RAiDER.cli.validators import enforce_time, enforce_wm, get_heights, get_los, get_query_region, parse_dates + with open(fname) as f: try: params = yaml.safe_load(f) except yaml.YAMLError as exc: print(exc) - raise ValueError( - f'Something is wrong with the yaml file {fname}') + raise ValueError(f'Something is wrong with the yaml file {fname}') # Drop any values not specified params = drop_nans(params) # Need to ensure that all the groups exist, even if they are not specified by the user - group_keys = ['date_group', 'time_group', 'aoi_group', - 'height_group', 'los_group', 'runtime_group'] + group_keys = ['date_group', 'time_group', 'aoi_group', 'height_group', 'los_group', 'runtime_group'] for key in group_keys: if key not in params.keys(): params[key] = {} @@ -92,8 +91,7 @@ def read_run_config_file(fname): run_config['date_list'] = parse_dates(AttributeDict(value)) if key == 'aoi_group': # in case a DEM is passed and should be used - dct_temp = {**AttributeDict(value), - **AttributeDict(params['height_group'])} + dct_temp = {**AttributeDict(value), **AttributeDict(params['height_group'])} run_config['aoi'] = get_query_region(AttributeDict(dct_temp)) if key == 'los_group': @@ -101,7 +99,7 @@ def read_run_config_file(fname): run_config['zref'] = AttributeDict(value).get('zref') if key == 'look_dir': if value.lower() not in ['right', 'left']: - raise ValueError(f"Unknown look direction {value}") + raise ValueError(f'Unknown look direction {value}') run_config['look_dir'] = value.lower() if key == 'cube_spacing_in_m': run_config[key] = float(value) if isinstance(value, str) else value @@ -145,13 +143,16 @@ def calcDelays(iargs=None): from RAiDER.checkArgs import checkArgs from RAiDER.delay import tropo_delay from RAiDER.utilFcns import get_nearest_wmtimes, writeDelays + examples = 'Examples of use:' \ '\n\t raider.py run_config_file.yaml' \ '\n\t raider.py --generate_config template' p = argparse.ArgumentParser( description=HELP_MESSAGE, - epilog=examples, formatter_class=argparse.RawDescriptionHelpFormatter) + epilog=examples, + formatter_class=argparse.RawDescriptionHelpFormatter + ) p.add_argument( '--download_only', @@ -163,7 +164,8 @@ def calcDelays(iargs=None): # run with a configuration file. group = p.add_mutually_exclusive_group(required=True) group.add_argument( - '--generate_config', '-g', + '--generate_config', + '-g', nargs='?', choices=[ 'template', @@ -171,7 +173,7 @@ def calcDelays(iargs=None): 'example_LA_GNSS', 'example_UK_isce', ], - help='Generate an example run configuration and exit' + help='Generate an example run configuration and exit', ) group.add_argument( 'run_config_file', @@ -184,10 +186,7 @@ def calcDelays(iargs=None): # Default example run configuration file ex_run_config_name = args.generate_config or 'template' - ex_run_config_dir = ( - Path(RAiDER.__file__).parent / - 'cli/examples' / ex_run_config_name - ) + ex_run_config_dir = Path(RAiDER.__file__).parent / 'cli/examples' / ex_run_config_name if args.generate_config is not None: for filename in ex_run_config_dir.glob('*'): @@ -208,7 +207,7 @@ def calcDelays(iargs=None): else: if not os.path.isfile(DEFAULT_RUN_CONFIG_PATH): msg = ( - "No run configuration file provided! Specify a run configuration " + 'No run configuration file provided! Specify a run configuration ' "file or have a 'raider.yaml' file in the current directory." ) p.print_usage() @@ -239,20 +238,14 @@ def calcDelays(iargs=None): # add a buffer determined by latitude for ray tracing if los.ray_trace(): - wm_bounds = aoi.calc_buffer_ray(los.getSensorDirection(), - lookDir=los.getLookDirection(), incAngle=30) + wm_bounds = aoi.calc_buffer_ray(los.getSensorDirection(), lookDir=los.getLookDirection(), incAngle=30) else: wm_bounds = aoi.bounds() model.set_latlon_bounds(wm_bounds, output_spacing=aoi.get_output_spacing()) wet_filenames = [] - for t, w, f in zip( - params['date_list'], - params['wetFilenames'], - params['hydroFilenames'] - ): - + for t, w, f in zip(params['date_list'], params['wetFilenames'], params['hydroFilenames']): ########################################################### # Weather model calculation ########################################################### @@ -263,13 +256,16 @@ def calcDelays(iargs=None): interp_method = params.get('interpolate_time') if interp_method is None: interp_method = 'none' - logger.warning('interp_method is not specified, defaulting to \'none\', i.e. nearest datetime for delay ' - 'calculation') + logger.warning( + "interp_method is not specified, defaulting to 'none', i.e. nearest datetime for delay calculation" + ) - if (interp_method != 'azimuth_time_grid'): - times = get_nearest_wmtimes( - t, [model.dtime() if model.dtime() is not None else 6][0] - ) if interp_method == 'center_time' else [t] + if interp_method != 'azimuth_time_grid': + times = ( + get_nearest_wmtimes(t, [model.dtime() if model.dtime() is not None else 6][0]) + if interp_method == 'center_time' + else [t] + ) elif interp_method == 'azimuth_time_grid': step = model.dtime() @@ -277,17 +273,13 @@ def calcDelays(iargs=None): # Will yield 2 or 3 dates depending if t is within 5 minutes of time step times = get_times_for_azimuth_interpolation(t, time_step_hours) else: - raise NotImplementedError('Only none, center_time, and azimuth_time_grid are accepted values for ' - 'interp_method.') + raise NotImplementedError( + 'Only none, center_time, and azimuth_time_grid are accepted values for interp_method.' + ) wfiles = [] for tt in times: try: - wfile = RAiDER.processWM.prepareWeatherModel( - model, - tt, - aoi.bounds(), - makePlots=params['verbose'] - ) + wfile = RAiDER.processWM.prepareWeatherModel(model, tt, aoi.bounds(), makePlots=params['verbose']) wfiles.append(wfile) except TryToKeepGoingError: @@ -299,16 +291,11 @@ def calcDelays(iargs=None): # log when something else happens and then continue with the next time except Exception as e: S, N, W, E = wm_bounds - logger.info( - 'Weather model point bounds are ' - f'{S:.2f}/{N:.2f}/{W:.2f}/{E:.2f}' - ) + logger.info('Weather model point bounds are ' f'{S:.2f}/{N:.2f}/{W:.2f}/{E:.2f}') logger.info(f'Query datetime: {tt}') logger.error(e) logger.error(f'Weather model files are: {wfiles}') - logger.error( - f'Downloading and/or preparation of {model._Name} failed.' - ) + logger.error(f'Downloading and/or preparation of {model._Name} failed.') continue # dont process the delays for download only @@ -316,28 +303,30 @@ def calcDelays(iargs=None): continue # Get the weather model file - weather_model_file = getWeatherFile( - wfiles, times, t, model._Name, interp_method) + weather_model_file = getWeatherFile(wfiles, times, t, model._Name, interp_method) # Now process the delays try: wet_delay, hydro_delay = tropo_delay( - t, weather_model_file, aoi, los, + t, + weather_model_file, + aoi, + los, height_levels=params['height_levels'], out_proj=params['output_projection'], - zref=params['zref'] + zref=params['zref'], ) except RuntimeError: - logger.exception("Datetime %s failed", t) + logger.exception('Datetime %s failed', t) continue # Different options depending on the inputs if los.is_Projected(): - out_filename = w.replace("_ztd", "_std") - f = f.replace("_ztd", "_std") + out_filename = w.replace('_ztd', '_std') + f = f.replace('_ztd', '_std') elif los.ray_trace(): - out_filename = w.replace("_std", "_ray") - f = f.replace("_std", "_ray") + out_filename = w.replace('_std', '_ray') + f = f.replace('_std', '_ray') else: out_filename = w @@ -349,29 +338,24 @@ def calcDelays(iargs=None): out_filename = out_filename.replace('wet', 'tropo') # data provenance: include metadata for model and times used - times_str = [t.strftime("%Y%m%dT%H:%M:%S") for t in sorted(times)] - ds = ds.assign_attrs(model_name=model._Name, - model_times_used=times_str, - interpolation_method=interp_method) + times_str = [t.strftime('%Y%m%dT%H:%M:%S') for t in sorted(times)] + ds = ds.assign_attrs(model_name=model._Name, model_times_used=times_str, interpolation_method=interp_method) if ext not in ['.nc', '.h5']: out_filename = f'{os.path.splitext(out_filename)[0]}.nc' - if out_filename.endswith(".nc"): - ds.to_netcdf(out_filename, mode="w") - elif out_filename.endswith(".h5"): - ds.to_netcdf(out_filename, engine="h5netcdf", - invalid_netcdf=True) + if out_filename.endswith('.nc'): + ds.to_netcdf(out_filename, mode='w') + elif out_filename.endswith('.h5'): + ds.to_netcdf(out_filename, engine='h5netcdf', invalid_netcdf=True) - logger.info( - '\nSuccessfully wrote delay cube to: %s\n', out_filename) + logger.info('\nSuccessfully wrote delay cube to: %s\n', out_filename) # Dataset returned: station files, radar_raster, geocoded_file else: if aoi.type() == 'station_file': out_filename = f'{os.path.splitext(out_filename)[0]}.csv' if aoi.type() in ['station_file', 'radar_rasters', 'geocoded_file']: - writeDelays(aoi, wet_delay, hydro_delay, out_filename, - f, outformat=params['raster_format']) + writeDelays(aoi, wet_delay, hydro_delay, out_filename, f, outformat=params['raster_format']) wet_filenames.append(out_filename) @@ -382,6 +366,7 @@ def calcDelays(iargs=None): def downloadGNSS() -> None: """Parse command line arguments using argparse.""" from RAiDER.gnss.downloadGNSSDelays import main as dlGNSS + p = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=""" \ @@ -412,26 +397,42 @@ def downloadGNSS() -> None: directory, across specified range of time (in YYMMDD YYMMDD) and specified time of day, and confined to specified geographic bounding box : downloadGNSSdelay.py --download --out products -y 20100101 20141231 --returntime '00:00:00' -b '39 40 -79 -78' - """) + """, + ) # Stations to check/download area = p.add_argument_group( - 'Stations to check/download. Can be a lat/lon bounding box or file, or will run the whole world if not specified') + 'Stations to check/download. Can be a lat/lon bounding box or file, or will run the whole world if not specified' + ) area.add_argument( - '--station_file', '-f', default=None, dest='station_file', - help=('Text file containing a list of 4-char station IDs separated by newlines')) + '--station_file', + '-f', + default=None, + dest='station_file', + help=('Text file containing a list of 4-char station IDs separated by newlines'), + ) area.add_argument( - '-b', '--bounding_box', dest='bounding_box', type=str, default=None, - help="Provide either valid shapefile or Lat/Lon Bounding SNWE. -- Example : '19 20 -99.5 -98.5'") + '-b', + '--bounding_box', + dest='bounding_box', + type=str, + default=None, + help="Provide either valid shapefile or Lat/Lon Bounding SNWE. -- Example : '19 20 -99.5 -98.5'", + ) area.add_argument( - '--gpsrepo', '-gr', default='UNR', dest='gps_repo', - help=('Specify GPS repository you wish to query. Currently supported archives: UNR.')) + '--gpsrepo', + '-gr', + default='UNR', + dest='gps_repo', + help=('Specify GPS repository you wish to query. Currently supported archives: UNR.'), + ) - misc = p.add_argument_group("Run parameters") + misc = p.add_argument_group('Run parameters') add_out(misc) misc.add_argument( - '--date', dest='dateList', + '--date', + dest='dateList', help=dedent("""\ Date to calculate delay. Can be a single date, a list of two dates (earlier, later) with 1-day interval, or a list of two dates and interval in days (earlier, later, interval). @@ -439,22 +440,27 @@ def downloadGNSS() -> None: YYYYMMDD or YYYYMMDD YYYYMMDD YYYYMMDD YYYYMMDD N - """), + """), nargs="+", action=DateListAction, type=date_type, - required=True + required=True, ) misc.add_argument( - '--returntime', dest='returnTime', + '--returntime', + dest='returnTime', help="Return delays closest to this specified time. If not specified, the GPS delays for all times will be returned. Input in 'HH:MM:SS', e.g. '16:00:00'", - default=None) + default=None, + ) misc.add_argument( '--download', help='Physically download data. Note this option is not necessary to proceed with statistical analyses, as data can be handled virtually in the program.', - action='store_true', dest='download', default=False) + action='store_true', + dest='download', + default=False, + ) add_cpus(misc) add_verbose(misc) @@ -462,67 +468,85 @@ def downloadGNSS() -> None: args = p.parse_args() dlGNSS(args) - return # ------------------------------------------------------------ prepFromGUNW.py def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: - p = argparse.ArgumentParser( description='Calculate a cube of interferometic delays for GUNW files', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) p.add_argument( '--bucket', - help='S3 bucket containing ARIA GUNW NetCDF file. Will be ignored if the --file argument is provided.' + help='S3 bucket containing ARIA GUNW NetCDF file. Will be ignored if the --file argument is provided.', ) p.add_argument( - '--bucket-prefix', default='', + '--bucket-prefix', + default='', help='S3 bucket prefix which may contain an ARIA GUNW NetCDF file to calculate delays for and which the final ' - 'ARIA GUNW NetCDF file will be upload to. Will be ignored if the --file argument is provided.' + 'ARIA GUNW NetCDF file will be upload to. Will be ignored if the --file argument is provided.', ) p.add_argument( '--input-bucket-prefix', help='S3 bucket prefix that contains an ARIA GUNW NetCDF file to calculate delays for. ' - 'If not provided, will look in --bucket-prefix for an ARIA GUNW NetCDF file. ' - 'Will be ignored if the --file argument is provided.' + 'If not provided, will look in --bucket-prefix for an ARIA GUNW NetCDF file. ' + 'Will be ignored if the --file argument is provided.', ) p.add_argument( - '-f', '--file', type=str, + '-f', + '--file', + type=str, help='1 ARIA GUNW netcdf file' ) p.add_argument( - '-m', '--weather-model', default='HRRR', type=str, - choices=['None'] + ALLOWED_MODELS, help='Weather model.' + '-m', + '--weather-model', + default='HRRR', + type=str, + choices=['None'] + ALLOWED_MODELS, + help='Weather model.' ) p.add_argument( - '-uid', '--api_uid', default=None, type=str, - help='Weather model API UID [uid, email, username], depending on model.' + '-uid', + '--api_uid', + default=None, + type=str, + help='Weather model API UID [uid, email, username], depending on model.', ) p.add_argument( - '-key', '--api_key', default=None, type=str, + '-key', + '--api_key', + default=None, + type=str, help='Weather model API KEY [key, password], depending on model.' ) p.add_argument( - '-interp', '--interpolate-time', default='azimuth_time_grid', type=str, + '-interp', + '--interpolate-time', + default='azimuth_time_grid', + type=str, choices=TIME_INTERPOLATION_METHODS, - help=('How to interpolate across model time steps. Possible options are: ' - '[\'none\', \'center_time\', \'azimuth_time_grid\'] ' - 'None: means nearest model time; center_time: linearly across center time; ' - 'Azimuth_time_grid: means every pixel is weighted with respect to azimuth time of S1;' - ) + help=( + 'How to interpolate across model time steps. Possible options are: ' + "['none', 'center_time', 'azimuth_time_grid'] " + 'None: means nearest model time; center_time: linearly across center time; ' + 'Azimuth_time_grid: means every pixel is weighted with respect to azimuth time of S1' + ), ) p.add_argument( - '-o', '--output-directory', default=os.getcwd(), type=str, - help='Directory to store results.' + '-o', + '--output-directory', + default=os.getcwd(), + type=str, help='Directory to store results.' ) iargs = p.parse_args(iargs) @@ -531,8 +555,7 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: iargs.input_bucket_prefix = iargs.bucket_prefix if iargs.interpolate_time not in ['none', 'center_time', 'azimuth_time_grid']: - raise ValueError( - 'interpolate_time arg must be in [\'none\', \'center_time\', \'azimuth_time_grid\']') + raise ValueError("interpolate_time arg must be in ['none', 'center_time', 'azimuth_time_grid']") if iargs.weather_model == 'None': # NOTE: HyP3's current step function implementation does not have a good way of conditionally @@ -546,24 +569,23 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: file_name = iargs.file.split('/')[-1] gunw_id = file_name.replace('.nc', '') if not RAiDER.aria.prepFromGUNW.check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id): - raise NoWeatherModelData( - 'The required HRRR data for time-grid interpolation is not available') + raise NoWeatherModelData('The required HRRR data for time-grid interpolation is not available') if not iargs.file and iargs.bucket: # only use GUNW ID for checking if HRRR available - iargs.file = aws.get_s3_file( - iargs.bucket, iargs.input_bucket_prefix, '.nc') + iargs.file = aws.get_s3_file(iargs.bucket, iargs.input_bucket_prefix, '.nc') if iargs.file is None: raise ValueError( - 'GUNW product file could not be found at' - f's3://{iargs.bucket}/{iargs.input_bucket_prefix}' + 'GUNW product file could not be found at' f's3://{iargs.bucket}/{iargs.input_bucket_prefix}' ) if iargs.weather_model == 'HRRR' and (iargs.interpolate_time == 'azimuth_time_grid'): file_name_str = str(iargs.file) gunw_nc_name = file_name_str.split('/')[-1] gunw_id = gunw_nc_name.replace('.nc', '') if not RAiDER.aria.prepFromGUNW.check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id): - print('The required HRRR data for time-grid interpolation is not available; returning None and not modifying GUNW dataset') + print( + 'The required HRRR data for time-grid interpolation is not available; returning None and not modifying GUNW dataset' + ) return # Download file to obtain metadata @@ -573,25 +595,20 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: # we include this within this portion of the control flow. print('Nothing to do because outside of weather model range') return - json_file_path = aws.get_s3_file( - iargs.bucket, iargs.input_bucket_prefix, '.json') + json_file_path = aws.get_s3_file(iargs.bucket, iargs.input_bucket_prefix, '.json') if json_file_path is None: raise ValueError( - 'GUNW metadata file could not be found at' - f's3://{iargs.bucket}/{iargs.input_bucket_prefix}' + 'GUNW metadata file could not be found at' f's3://{iargs.bucket}/{iargs.input_bucket_prefix}' ) json_data = json.load(open(json_file_path)) - json_data['metadata'].setdefault( - 'weather_model', []).append(iargs.weather_model) + json_data['metadata'].setdefault('weather_model', []).append(iargs.weather_model) json.dump(json_data, open(json_file_path, 'w')) # also get browse image -- if RAiDER is running in its own HyP3 job, the browse image will be needed for ingest - browse_file_path = aws.get_s3_file( - iargs.bucket, iargs.input_bucket_prefix, '.png') + browse_file_path = aws.get_s3_file(iargs.bucket, iargs.input_bucket_prefix, '.png') if browse_file_path is None: raise ValueError( - 'GUNW browse image could not be found at' - f's3://{iargs.bucket}/{iargs.input_bucket_prefix}' + 'GUNW browse image could not be found at' f's3://{iargs.bucket}/{iargs.input_bucket_prefix}' ) elif not iargs.file: @@ -607,18 +624,17 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: assert len(cube_filenames) == 2, 'Incorrect number of delay files written.' # calculate the interferometric phase and write it out - ds = RAiDER.aria.calcGUNW.tropo_gunw_slc(cube_filenames, - iargs.file, - wavelength, - ) + ds = RAiDER.aria.calcGUNW.tropo_gunw_slc( + cube_filenames, + iargs.file, + wavelength, + ) # upload to s3 if iargs.bucket: aws.upload_file_to_s3(iargs.file, iargs.bucket, iargs.bucket_prefix) - aws.upload_file_to_s3( - json_file_path, iargs.bucket, iargs.bucket_prefix) - aws.upload_file_to_s3( - browse_file_path, iargs.bucket, iargs.bucket_prefix) + aws.upload_file_to_s3(json_file_path, iargs.bucket, iargs.bucket_prefix) + aws.upload_file_to_s3(browse_file_path, iargs.bucket, iargs.bucket_prefix) return ds @@ -634,8 +650,9 @@ def combineZTDFiles() -> None: combineDelayFiles(args.raider_file, loc=args.raider_folder) if not os.path.exists(args.gnss_file): - combineDelayFiles(args.gnss_file, loc=args.gnss_folder, source='GNSS', - ref=args.raider_file, col_name=args.column_name) + combineDelayFiles( + args.gnss_file, loc=args.gnss_folder, source='GNSS', ref=args.raider_file, col_name=args.column_name + ) if args.gnss_file is not None: main( @@ -644,7 +661,7 @@ def combineZTDFiles() -> None: col_name=args.column_name, raider_delay=args.raider_column_name, outName=args.out_name, - localTime=args.local_time + localTime=args.local_time, ) @@ -665,11 +682,10 @@ def getWeatherFile(wfiles, times, t, model, interp_method='none'): try: Nfiles_expected = EXPECTED_NUM_FILES[interp_method] except KeyError: - raise ValueError( - f'getWeatherFile: interp_method {interp_method} is not known') + raise ValueError(f'getWeatherFile: interp_method {interp_method} is not known') - Nmatch = (Nfiles_expected == Nfiles) - Tmatch = (Nfiles == Ntimes) + Nmatch = Nfiles_expected == Nfiles + Tmatch = Nfiles == Ntimes # Case 1: no files downloaded if Nfiles == 0: @@ -677,54 +693,43 @@ def getWeatherFile(wfiles, times, t, model, interp_method='none'): return None # Case 2 - nearest weather model time is requested and retrieved - if (interp_method == 'none'): + if interp_method == 'none': weather_model_file = wfiles[0] - elif (interp_method == 'center_time'): - + elif interp_method == 'center_time': if Nmatch: # Case 3: two weather files downloaded - weather_model_file = combine_weather_files( - wfiles, - t, - model, - interp_method='center_time' - ) + weather_model_file = combine_weather_files(wfiles, t, model, interp_method='center_time') elif Tmatch: # Case 4: Exact time is available without interpolation - logger.warning( - 'Time interpolation is not needed as exact time is available') + logger.warning('Time interpolation is not needed as exact time is available') weather_model_file = wfiles[0] elif Nfiles == 1: # Case 5: one file does not download for some reason logger.warning( - 'getWeatherFile: One datetime is not available to download, defaulting to nearest available date') + 'getWeatherFile: One datetime is not available to download, defaulting to nearest available date' + ) weather_model_file = wfiles[0] else: raise WrongNumberOfFiles(Nfiles_expected, Nfiles) elif (interp_method) == 'azimuth_time_grid': - if Nmatch or Tmatch: # Case 6: all files downloaded - weather_model_file = combine_weather_files( - wfiles, - t, - model, - interp_method='azimuth_time_grid' - ) + weather_model_file = combine_weather_files(wfiles, t, model, interp_method='azimuth_time_grid') else: raise WrongNumberOfFiles(Nfiles_expected, Nfiles) # Case 7 - Anything else errors out else: N = len(wfiles) - raise NotImplementedError(f'The {interp_method} with {N} retrieved weather model files was not well posed ' - 'for the current workflow.') + raise NotImplementedError( + f'The {interp_method} with {N} retrieved weather model files was not well posed ' + 'for the current workflow.' + ) return weather_model_file def combine_weather_files(wfiles, t, model, interp_method='center_time'): """Interpolate downloaded weather files and save to a single file.""" - STYLE = {'center_time': '_timeInterp_', - 'azimuth_time_grid': '_timeInterpAziGrid_'} + STYLE = {'center_time': '_timeInterp_', 'azimuth_time_grid': '_timeInterpAziGrid_'} # read the individual datetime datasets datasets = [xr.open_dataset(f) for f in wfiles] @@ -732,8 +737,7 @@ def combine_weather_files(wfiles, t, model, interp_method='center_time'): # Pull the datetimes from the datasets times = [] for ds in datasets: - times.append(datetime.datetime.strptime( - ds.attrs['datetime'], '%Y_%m_%dT%H_%M_%S')) + times.append(datetime.datetime.strptime(ds.attrs['datetime'], '%Y_%m_%dT%H_%M_%S')) if len(times) == 0: raise NoWeatherModelData() @@ -755,9 +759,11 @@ def combine_weather_files(wfiles, t, model, interp_method='center_time'): # Give the weighted combination a new file name weather_model_file = os.path.join( os.path.dirname(wfiles[0]), - os.path.basename(wfiles[0]).split('_')[0] + '_' + - t.strftime('%Y_%m_%dT%H_%M_%S') + STYLE[interp_method] + - '_'.join(wfiles[0].split('_')[-4:]), + os.path.basename(wfiles[0]).split('_')[0] + + '_' + + t.strftime('%Y_%m_%dT%H_%M_%S') + + STYLE[interp_method] + + '_'.join(wfiles[0].split('_')[-4:]), ) # write the combined results to disk @@ -774,8 +780,7 @@ def combine_files_using_azimuth_time(wfiles, t, times): # Pull the datetimes from the datasets times = [] for ds in datasets: - times.append(datetime.datetime.strptime( - ds.attrs['datetime'], '%Y_%m_%dT%H_%M_%S')) + times.append(datetime.datetime.strptime(ds.attrs['datetime'], '%Y_%m_%dT%H_%M_%S')) model = datasets[0].attrs['model_name'] @@ -793,8 +798,11 @@ def combine_files_using_azimuth_time(wfiles, t, times): # Give the weighted combination a new file name weather_model_file = os.path.join( os.path.dirname(wfiles[0]), - os.path.basename(wfiles[0]).split('_')[0] + '_' + t.strftime( - '%Y_%m_%dT%H_%M_%S') + '_timeInterpAziGrid_' + '_'.join(wfiles[0].split('_')[-4:]), + os.path.basename(wfiles[0]).split('_')[0] + + '_' + + t.strftime('%Y_%m_%dT%H_%M_%S') + + '_timeInterpAziGrid_' + + '_'.join(wfiles[0].split('_')[-4:]), ) # write the combined results to disk @@ -806,14 +814,12 @@ def combine_files_using_azimuth_time(wfiles, t, times): def get_weights_time_interp(times, t): """Calculate weights for time interpolation using simple inverse linear weighting.""" date1, date2 = times - wgts = [1 - get_dt(t, date1) / get_dt(date2, date1), 1 - - get_dt(date2, t) / get_dt(date2, date1)] + wgts = [1 - get_dt(t, date1) / get_dt(date2, date1), 1 - get_dt(date2, t) / get_dt(date2, date1)] try: assert np.isclose(np.sum(wgts), 1) except AssertionError: - logger.error( - 'Time interpolation weights do not sum to one; something is off with query datetime: %s', t) + logger.error('Time interpolation weights do not sum to one; something is off with query datetime: %s', t) return None return wgts @@ -838,13 +844,10 @@ def get_time_grid_for_aztime_interp(datasets, t, model): hgt = np.broadcast_to(z_1d[:, None, None], (m, n, p)) else: - raise NotImplementedError( - 'Azimuth Time is currently only implemented for HRRR') + raise NotImplementedError('Azimuth Time is currently only implemented for HRRR') - time_grid = get_s1_azimuth_time_grid( - lon, lat, hgt, t) # This is the acq time from loop + time_grid = get_s1_azimuth_time_grid(lon, lat, hgt, t) # This is the acq time from loop if np.any(np.isnan(time_grid)): - raise ValueError( - 'The Time Grid return nans meaning no orbit was downloaded.') + raise ValueError('The Time Grid return nans meaning no orbit was downloaded.') return time_grid diff --git a/tools/RAiDER/cli/statsPlot.py b/tools/RAiDER/cli/statsPlot.py index ec8fb96de..7a47b08ac 100755 --- a/tools/RAiDER/cli/statsPlot.py +++ b/tools/RAiDER/cli/statsPlot.py @@ -35,7 +35,9 @@ def create_parser(): """Parse command line arguments using argparse.""" - parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=""" + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=""" Perform basic statistical analyses concerning the spatiotemporal distribution of zenith delays. Specifically, make any of the following specified plot(s): @@ -51,117 +53,340 @@ def create_parser(): Example call to plot gridded station variogram in a specific time interval and through explicitly the summer seasons: raiderStats.py -f -grid_delay_mean -ti '2016-01-01 2018-01-01' --seasonalinterval '06-21 09-21' -variogramplot -""") +""", + ) # User inputs - userinps = parser.add_argument_group( - 'User inputs/options for which especially careful review is recommended') - userinps.add_argument('-f', '--file', dest='fname', - type=str, required=True, help='Final output file generated from downloadGNSSDelays.py which contains GPS zenith delays for a specified time period and spatial footprint. ') - userinps.add_argument('-c', '--column_name', dest='col_name', type=str, default='ZTD', - help='Name of the input column to plot. Input assumed to be in units of meters') - userinps.add_argument('-u', '--unit', dest='unit', type=str, default='m', - help='Specified output unit (as distance or time), by default m. Input unit assumed to be m following convention in downloadGNSSDelays.py. Refer to "convert_SI" for supported units. Note if you specify time unit here, you must specify input for "--obs_errlimit" to be in units of m') - userinps.add_argument('-w', '--workdir', dest='workdir', default='./', - help='Specify directory to deposit all outputs. Default is local directory where script is launched.') + userinps = parser.add_argument_group('User inputs/options for which especially careful review is recommended') + userinps.add_argument( + '-f', + '--file', + dest='fname', + type=str, + required=True, + help='Final output file generated from downloadGNSSDelays.py which contains GPS zenith delays for a specified time period and spatial footprint. ', + ) + userinps.add_argument( + '-c', + '--column_name', + dest='col_name', + type=str, + default='ZTD', + help='Name of the input column to plot. Input assumed to be in units of meters', + ) + userinps.add_argument( + '-u', + '--unit', + dest='unit', + type=str, + default='m', + help='Specified output unit (as distance or time), by default m. Input unit assumed to be m following convention in downloadGNSSDelays.py. Refer to "convert_SI" for supported units. Note if you specify time unit here, you must specify input for "--obs_errlimit" to be in units of m', + ) + userinps.add_argument( + '-w', + '--workdir', + dest='workdir', + default='./', + help='Specify directory to deposit all outputs. Default is local directory where script is launched.', + ) add_cpus(userinps) - userinps.add_argument('-verbose', '--verbose', action='store_true', dest='verbose', - help="Run in verbose (debug) mode. Default False") + userinps.add_argument( + '-verbose', + '--verbose', + action='store_true', + dest='verbose', + help='Run in verbose (debug) mode. Default False' + ) # Spatiotemporal subset options - dtsubsets = parser.add_argument_group( - 'Controls for spatiotemporal subsetting.') - dtsubsets.add_argument('-b', '--bounding_box', dest='bounding_box', type=str, default=None, - help="Provide either valid shapefile or Lat/Lon Bounding SNWE. -- Example : '19 20 -99.5 -98.5'") - dtsubsets.add_argument('-sp', '--spacing', dest='spacing', type=float, default='1', - help='Specify spacing of grid-cells for statistical analyses. By default 1 deg.') - dtsubsets.add_argument('-ti', '--timeinterval', dest='timeinterval', type=str, default=None, - help="Subset in time by specifying earliest YYYY-MM-DD date followed by latest date YYYY-MM-DD. -- Example : '2016-01-01 2019-01-01'.") - dtsubsets.add_argument('-si', '--seasonalinterval', dest='seasonalinterval', type=str, default=None, - help="Subset in by an specific interval for each year by specifying earliest MM-DD time followed by latest MM-DD time. -- Example : '03-21 06-21'.") - dtsubsets.add_argument('-oe', '--obs_errlimit', dest='obs_errlimit', type=float, default='inf', - help="Observation error threshold to discard observations with large uncertainties.") + dtsubsets = parser.add_argument_group('Controls for spatiotemporal subsetting.') + dtsubsets.add_argument( + '-b', + '--bounding_box', + dest='bounding_box', + type=str, + default=None, + help="Provide either valid shapefile or Lat/Lon Bounding SNWE. -- Example : '19 20 -99.5 -98.5'", + ) + dtsubsets.add_argument( + '-sp', + '--spacing', + dest='spacing', + type=float, + default='1', + help='Specify spacing of grid-cells for statistical analyses. By default 1 deg.', + ) + dtsubsets.add_argument( + '-ti', + '--timeinterval', + dest='timeinterval', + type=str, + default=None, + help="Subset in time by specifying earliest YYYY-MM-DD date followed by latest date YYYY-MM-DD. -- Example : '2016-01-01 2019-01-01'.", + ) + dtsubsets.add_argument( + '-si', + '--seasonalinterval', + dest='seasonalinterval', + type=str, + default=None, + help="Subset in by an specific interval for each year by specifying earliest MM-DD time followed by latest MM-DD time. -- Example : '03-21 06-21'.", + ) + dtsubsets.add_argument( + '-oe', + '--obs_errlimit', + dest='obs_errlimit', + type=float, + default='inf', + help='Observation error threshold to discard observations with large uncertainties.', + ) # Plot formatting/options - pltformat = parser.add_argument_group( - 'Optional controls for plot formatting/options.') - pltformat.add_argument('-figdpi', '--figdpi', dest='figdpi', type=int, - default=100, help='DPI to use for saving figures') - pltformat.add_argument('-title', '--user_title', dest='user_title', type=str, - default=None, help='Specify custom title for plots.') - pltformat.add_argument('-fmt', '--plot_format', dest='plot_fmt', type=str, - default='png', help='Plot format to use for saving figures') - pltformat.add_argument('-cb', '--color_bounds', dest='cbounds', type=str, - default=None, help='List of two floats to use as color axis bounds') - pltformat.add_argument('-cp', '--colorpercentile', dest='colorpercentile', type=float, default=None, nargs=2, - help='Set low and upper percentile for plot colorbars. By default 25%% and 95%%, respectively.') - pltformat.add_argument('-cm', '--colormap', dest='usr_colormap', type=str, default='hot_r', - help='Specify matplotlib colorbar.') - pltformat.add_argument('-dt', '--densitythreshold', dest='densitythreshold', type=int, default='10', - help='For variogram plots, given grid-cell is only valid if it contains this specified threshold of stations. By default 10 stations.') - pltformat.add_argument('-sg', '--stationsongrids', dest='stationsongrids', action='store_true', - help='In gridded plots, superimpose your gridded array with a scatterplot of station locations.') - pltformat.add_argument('-dg', '--drawgridlines', dest='drawgridlines', - action='store_true', help='Draw gridlines on gridded plots.') - pltformat.add_argument('-tl', '--time_lines', dest='time_lines', - action='store_true', help='Draw central longitudinal lines with respect to datetime. Most useful for local-time analyses.') - pltformat.add_argument('-plotall', '--plotall', action='store_true', dest='plotall', - help="Generate all supported plots, including variogram plots.") - pltformat.add_argument('-min_span', '--min_span', dest='min_span', type=float, - default=[2, 0.6], nargs=2, help="Minimum TS span (years) and minimum fractional observations in span (fraction) imposed for seasonal amplitude/phase analyses to be performed for a given station.") - pltformat.add_argument('-period_limit', '--period_limit', dest='period_limit', type=float, - default=0., help="period limit (years) imposed for seasonal amplitude/phase analyses to be performed for a given station.") + pltformat = parser.add_argument_group('Optional controls for plot formatting/options.') + pltformat.add_argument( + '-figdpi', + '--figdpi', + dest='figdpi', + type=int, + default=100, + help='DPI to use for saving figures' + ) + pltformat.add_argument( + '-title', + '--user_title', + dest='user_title', + type=str, + default=None, + help='Specify custom title for plots.' + ) + pltformat.add_argument( + '-fmt', + '--plot_format', + dest='plot_fmt', + type=str, + default='png', + help='Plot format to use for saving figures' + ) + pltformat.add_argument( + '-cb', + '--color_bounds', + dest='cbounds', + type=str, + default=None, + help='List of two floats to use as color axis bounds', + ) + pltformat.add_argument( + '-cp', + '--colorpercentile', + dest='colorpercentile', + type=float, + default=None, + nargs=2, + help='Set low and upper percentile for plot colorbars. By default 25%% and 95%%, respectively.', + ) + pltformat.add_argument( + '-cm', '--colormap', dest='usr_colormap', type=str, default='hot_r', help='Specify matplotlib colorbar.' + ) + pltformat.add_argument( + '-dt', + '--densitythreshold', + dest='densitythreshold', + type=int, + default='10', + help='For variogram plots, given grid-cell is only valid if it contains this specified threshold of stations. By default 10 stations.', + ) + pltformat.add_argument( + '-sg', + '--stationsongrids', + dest='stationsongrids', + action='store_true', + help='In gridded plots, superimpose your gridded array with a scatterplot of station locations.', + ) + pltformat.add_argument( + '-dg', '--drawgridlines', dest='drawgridlines', action='store_true', help='Draw gridlines on gridded plots.' + ) + pltformat.add_argument( + '-tl', + '--time_lines', + dest='time_lines', + action='store_true', + help='Draw central longitudinal lines with respect to datetime. Most useful for local-time analyses.', + ) + pltformat.add_argument( + '-plotall', + '--plotall', + action='store_true', + dest='plotall', + help='Generate all supported plots, including variogram plots.', + ) + pltformat.add_argument( + '-min_span', + '--min_span', + dest='min_span', + type=float, + default=[2, 0.6], + nargs=2, + help='Minimum TS span (years) and minimum fractional observations in span (fraction) imposed for seasonal amplitude/phase analyses to be performed for a given station.', + ) + pltformat.add_argument( + '-period_limit', + '--period_limit', + dest='period_limit', + type=float, + default=0.0, + help='period limit (years) imposed for seasonal amplitude/phase analyses to be performed for a given station.', + ) # All plot types # Station scatter-plots - pltscatter = parser.add_argument_group( - 'Supported types of individual station scatter-plots.') - pltscatter.add_argument('-station_distribution', '--station_distribution', - action='store_true', dest='station_distribution', help="Plot station distribution.") - pltscatter.add_argument('-station_delay_mean', '--station_delay_mean', - action='store_true', dest='station_delay_mean', help="Plot station mean delay.") - pltscatter.add_argument('-station_delay_median', '--station_delay_median', - action='store_true', dest='station_delay_median', help="Plot station median delay.") - pltscatter.add_argument('-station_delay_stdev', '--station_delay_stdev', - action='store_true', dest='station_delay_stdev', help="Plot station delay stdev.") - pltscatter.add_argument('-station_seasonal_phase', '--station_seasonal_phase', - action='store_true', dest='station_seasonal_phase', help="Plot station delay phase/amplitude.") - pltscatter.add_argument('-phaseamp_per_station', '--phaseamp_per_station', - action='store_true', dest='phaseamp_per_station', help="Save debug figures of curve-fit vs data per station.") + pltscatter = parser.add_argument_group('Supported types of individual station scatter-plots.') + pltscatter.add_argument( + '-station_distribution', + '--station_distribution', + action='store_true', + dest='station_distribution', + help='Plot station distribution.', + ) + pltscatter.add_argument( + '-station_delay_mean', + '--station_delay_mean', + action='store_true', + dest='station_delay_mean', + help='Plot station mean delay.', + ) + pltscatter.add_argument( + '-station_delay_median', + '--station_delay_median', + action='store_true', + dest='station_delay_median', + help='Plot station median delay.', + ) + pltscatter.add_argument( + '-station_delay_stdev', + '--station_delay_stdev', + action='store_true', + dest='station_delay_stdev', + help='Plot station delay stdev.', + ) + pltscatter.add_argument( + '-station_seasonal_phase', + '--station_seasonal_phase', + action='store_true', + dest='station_seasonal_phase', + help='Plot station delay phase/amplitude.', + ) + pltscatter.add_argument( + '-phaseamp_per_station', + '--phaseamp_per_station', + action='store_true', + dest='phaseamp_per_station', + help='Save debug figures of curve-fit vs data per station.', + ) # Gridded plots pltgrids = parser.add_argument_group('Supported types of gridded plots.') - pltgrids.add_argument('-grid_heatmap', '--grid_heatmap', action='store_true', - dest='grid_heatmap', help="Plot gridded station heatmap.") - pltgrids.add_argument('-grid_delay_mean', '--grid_delay_mean', action='store_true', - dest='grid_delay_mean', help="Plot gridded station-wise mean delay.") - pltgrids.add_argument('-grid_delay_median', '--grid_delay_median', action='store_true', - dest='grid_delay_median', help="Plot gridded station-wise median delay.") - pltgrids.add_argument('-grid_delay_stdev', '--grid_delay_stdev', action='store_true', - dest='grid_delay_stdev', help="Plot gridded station-wise delay stdev.") - pltgrids.add_argument('-grid_seasonal_phase', '--grid_seasonal_phase', action='store_true', - dest='grid_seasonal_phase', help="Plot gridded station-wise delay phase/amplitude.") - pltgrids.add_argument('-grid_delay_absolute_mean', '--grid_delay_absolute_mean', action='store_true', - dest='grid_delay_absolute_mean', help="Plot absolute gridded station mean delay.") - pltgrids.add_argument('-grid_delay_absolute_median', '--grid_delay_absolute_median', action='store_true', - dest='grid_delay_absolute_median', help="Plot absolute gridded station median delay.") - pltgrids.add_argument('-grid_delay_absolute_stdev', '--grid_delay_absolute_stdev', action='store_true', - dest='grid_delay_absolute_stdev', help="Plot absolute gridded station delay stdev.") - pltgrids.add_argument('-grid_seasonal_absolute_phase', '--grid_seasonal_absolute_phase', action='store_true', - dest='grid_seasonal_absolute_phase', help="Plot absolute gridded station delay phase/amplitude.") - pltgrids.add_argument('-grid_to_raster', '--grid_to_raster', action='store_true', - dest='grid_to_raster', help="Save gridded array as raster. May directly load/plot in successive script call.") + pltgrids.add_argument( + '-grid_heatmap', + '--grid_heatmap', + action='store_true', + dest='grid_heatmap', + help='Plot gridded station heatmap.', + ) + pltgrids.add_argument( + '-grid_delay_mean', + '--grid_delay_mean', + action='store_true', + dest='grid_delay_mean', + help='Plot gridded station-wise mean delay.', + ) + pltgrids.add_argument( + '-grid_delay_median', + '--grid_delay_median', + action='store_true', + dest='grid_delay_median', + help='Plot gridded station-wise median delay.', + ) + pltgrids.add_argument( + '-grid_delay_stdev', + '--grid_delay_stdev', + action='store_true', + dest='grid_delay_stdev', + help='Plot gridded station-wise delay stdev.', + ) + pltgrids.add_argument( + '-grid_seasonal_phase', + '--grid_seasonal_phase', + action='store_true', + dest='grid_seasonal_phase', + help='Plot gridded station-wise delay phase/amplitude.', + ) + pltgrids.add_argument( + '-grid_delay_absolute_mean', + '--grid_delay_absolute_mean', + action='store_true', + dest='grid_delay_absolute_mean', + help='Plot absolute gridded station mean delay.', + ) + pltgrids.add_argument( + '-grid_delay_absolute_median', + '--grid_delay_absolute_median', + action='store_true', + dest='grid_delay_absolute_median', + help='Plot absolute gridded station median delay.', + ) + pltgrids.add_argument( + '-grid_delay_absolute_stdev', + '--grid_delay_absolute_stdev', + action='store_true', + dest='grid_delay_absolute_stdev', + help='Plot absolute gridded station delay stdev.', + ) + pltgrids.add_argument( + '-grid_seasonal_absolute_phase', + '--grid_seasonal_absolute_phase', + action='store_true', + dest='grid_seasonal_absolute_phase', + help='Plot absolute gridded station delay phase/amplitude.', + ) + pltgrids.add_argument( + '-grid_to_raster', + '--grid_to_raster', + action='store_true', + dest='grid_to_raster', + help='Save gridded array as raster. May directly load/plot in successive script call.', + ) # Variogram plots pltvario = parser.add_argument_group('Supported types of variogram plots.') - pltvario.add_argument('-variogramplot', '--variogramplot', action='store_true', - dest='variogramplot', help="Plot gridded station variogram.") - pltvario.add_argument('-binnedvariogram', '--binnedvariogram', action='store_true', dest='binnedvariogram', - help="Apply experimental variogram fit to total binned empirical variograms for each time slice. Default is to pass total unbinned empiricial variogram.") - pltvario.add_argument('-variogram_per_timeslice', '--variogram_per_timeslice', action='store_true', dest='variogram_per_timeslice', - help="Generate variogram plots per gridded station AND time-slice.") - pltvario.add_argument('-variogram_errlimit', '--variogram_errlimit', dest='variogram_errlimit', type=float, default='inf', - help="Variogram RMSE threshold to discard grid-cells with large uncertainties.") + pltvario.add_argument( + '-variogramplot', + '--variogramplot', + action='store_true', + dest='variogramplot', + help='Plot gridded station variogram.', + ) + pltvario.add_argument( + '-binnedvariogram', + '--binnedvariogram', + action='store_true', + dest='binnedvariogram', + help='Apply experimental variogram fit to total binned empirical variograms for each time slice. Default is to pass total unbinned empiricial variogram.', + ) + pltvario.add_argument( + '-variogram_per_timeslice', + '--variogram_per_timeslice', + action='store_true', + dest='variogram_per_timeslice', + help='Generate variogram plots per gridded station AND time-slice.', + ) + pltvario.add_argument( + '-variogram_errlimit', + '--variogram_errlimit', + dest='variogram_errlimit', + type=float, + default='inf', + help='Variogram RMSE threshold to discard grid-cells with large uncertainties.', + ) return parser @@ -173,8 +398,7 @@ def cmd_line_parse(iargs=None): def convert_SI(val, unit_in, unit_out): """Convert input to desired units.""" - SI = {'mm': 0.001, 'cm': 0.01, 'm': 1.0, 'km': 1000., - 'mm^2': 1e-6, 'cm^2': 1e-4, 'm^2': 1.0, 'km^2': 1e+6} + SI = {'mm': 0.001, 'cm': 0.01, 'm': 1.0, 'km': 1000.0, 'mm^2': 1e-6, 'cm^2': 1e-4, 'm^2': 1.0, 'km^2': 1e6} # avoid conversion if output unit in time if unit_out in ['minute', 'hour', 'day', 'year']: @@ -187,7 +411,7 @@ def convert_SI(val, unit_in, unit_out): # check if output spatial unit is supported if unit_out not in SI: - raise ValueError(f"User-specified output unit {unit_out} not recognized.") + raise ValueError(f'User-specified output unit {unit_out} not recognized.') return val * SI[unit_in] / SI[unit_out] @@ -205,12 +429,22 @@ def midpoint(p1, p2): dy = math.cos(lat2) * math.sin(dlon) lon3 = lon1 + math.atan2(dy, math.cos(lat1) + dx) - return (int(math.degrees(lon3))) + return int(math.degrees(lon3)) -def save_gridfile(df, gridfile_type, fname, plotbbox, spacing, unit, - colorbarfmt='%.2f', stationsongrids=False, time_lines=False, - dtype="float32", noData=np.nan): +def save_gridfile( + df, + gridfile_type, + fname, + plotbbox, + spacing, + unit, + colorbarfmt='%.2f', + stationsongrids=False, + time_lines=False, + dtype='float32', + noData=np.nan, +): """Function to save gridded-arrays as GDAL-readable file.""" # Pass metadata metadata_dict = {} @@ -233,11 +467,18 @@ def save_gridfile(df, gridfile_type, fname, plotbbox, spacing, unit, metadata_dict['time_lines'] = 'False' # Write data to file - transform = Affine(spacing, 0., plotbbox[0], 0., -1*spacing, plotbbox[-1]) - with rasterio.open(fname, mode="w", count=1, - width=df.shape[1], height=df.shape[0], - dtype=dtype, nodata=noData, - crs='+proj=latlong', transform=transform) as dst: + transform = Affine(spacing, 0.0, plotbbox[0], 0.0, -1 * spacing, plotbbox[-1]) + with rasterio.open( + fname, + mode='w', + count=1, + width=df.shape[1], + height=df.shape[0], + dtype=dtype, + nodata=noData, + crs='+proj=latlong', + transform=transform, + ) as dst: dst.update_tags(0, **metadata_dict) dst.write(df, 1) @@ -301,7 +542,20 @@ def load_gridfile(fname, unit): class VariogramAnalysis: """Class which ingests dataframe output from 'RaiderStats' class and performs variogram analysis.""" - def __init__(self, filearg, gridpoints, col_name, unit='m', workdir='./', seasonalinterval=None, densitythreshold=10, binnedvariogram=False, numCPUs=8, variogram_per_timeslice=False, variogram_errlimit='inf') -> None: + def __init__( + self, + filearg, + gridpoints, + col_name, + unit='m', + workdir='./', + seasonalinterval=None, + densitythreshold=10, + binnedvariogram=False, + numCPUs=8, + variogram_per_timeslice=False, + variogram_errlimit='inf', + ) -> None: self.df = filearg self.col_name = col_name self.unit = unit @@ -317,6 +571,7 @@ def __init__(self, filearg, gridpoints, col_name, unit='m', workdir='./', season def _get_samples(self, data, Nsamp=1000): """Pull samples from a 2D image for variogram analysis.""" import random + if len(data) < self.densitythreshold: logger.warning('Less than {} points for this gridcell', self.densitythreshold) logger.info('Will pass empty list') @@ -344,6 +599,7 @@ def _get_XY(self, x2d, y2d, indpars): def _get_distances(self, XY): """Return the distances between each point in a list of points.""" from scipy.spatial.distance import cdist + return np.diag(cdist(XY[:, :, 0], XY[:, :, 1], metric='euclidean')) def _get_variogram(self, XY, xy=None): @@ -367,8 +623,7 @@ def _emp_vario(self, x, y, data, Nsamp=1000): samples, indpars = self._get_samples(data, Nsamp) x, y = self._get_XY(x, y, indpars) - dists = self._get_distances( - np.array([[x[:, 0], y[:, 0]], [x[:, 1], y[:, 1]]]).T) + dists = self._get_distances(np.array([[x[:, 0], y[:, 0]], [x[:, 1], y[:, 1]]]).T) vario = self._get_variogram(samples[:, 0], samples[:, 1]) return dists, vario @@ -377,8 +632,8 @@ def _binned_vario(self, hEff, rawVario, xBin=None): """Return a binned empirical variogram.""" if xBin is None: with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message="All-NaN slice encountered") - xBin = np.linspace(0, np.nanmax(hEff) * .67, 20) + warnings.filterwarnings('ignore', message='All-NaN slice encountered') + xBin = np.linspace(0, np.nanmax(hEff) * 0.67, 20) nBins = len(xBin) - 1 hExp, expVario = [], [] @@ -388,7 +643,7 @@ def _binned_vario(self, hEff, rawVario, xBin=None): # circumvent indexing try: with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message="Mean of empty slice") + warnings.filterwarnings('ignore', message='Mean of empty slice') hExp.append(np.nanmean(hEff[iBinMask])) expVario.append(np.nanmean(rawVario[iBinMask])) except BaseException: # TODO: Which error(s)? @@ -406,17 +661,15 @@ def _fit_vario(self, dists, vario, model=None, x0=None, Nparm=None, ub=None): from scipy.optimize import least_squares def resid(x, d, v, m): - return (m(x, d) - v) + return m(x, d) - v if ub is None: with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message="All-NaN slice encountered") - ub = np.array([np.nanmax(dists) * 0.8, np.nanmax(vario) - * 0.8, np.nanmax(vario) * 0.8]) + warnings.filterwarnings('ignore', message='All-NaN slice encountered') + ub = np.array([np.nanmax(dists) * 0.8, np.nanmax(vario) * 0.8, np.nanmax(vario) * 0.8]) if x0 is None and Nparm is None: - raise RuntimeError( - 'Must specify either x0 or the number of model parameters') + raise RuntimeError('Must specify either x0 or the number of model parameters') if x0 is not None: lb = np.zeros(len(x0)) if Nparm is not None: @@ -428,12 +681,17 @@ def resid(x, d, v, m): d = dists[~mask].copy() v = vario[~mask].copy() - res_robust = least_squares(resid, x0, bounds=bounds, - loss='soft_l1', f_scale=0.1, - args=(d, v, model)) + res_robust = least_squares( + resid, + x0, + bounds=bounds, + loss='soft_l1', + f_scale=0.1, + args=(d, v, model), + ) with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message="All-NaN slice encountered") + warnings.filterwarnings('ignore', message='All-NaN slice encountered') d_test = np.linspace(0, np.nanmax(dists), 100) # v_test is my y., # res_robust.x =a, b, c, where a = range, b = sill, and c = nugget model, d_test=x v_test = model(res_robust.x, d_test) @@ -446,7 +704,7 @@ def __exponential__(self, parms, h, nugget=False): # a = range, b = sill, c = nugget model a, b, c = parms with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message="overflow encountered in true_divide") + warnings.filterwarnings('ignore', message='overflow encountered in true_divide') if nugget: return b * (1 - np.exp(-h / a)) + c else: @@ -472,38 +730,54 @@ def _append_variogram(self, grid_ind, grid_subset): # If insufficient sample size, skip slice and record occurence if len(np.array(grid_subset[grid_subset['Date'] == j][self.col_name])) < self.densitythreshold: # Record skipped [gridnode, timeslice] - self.skipped_slices.append([grid_ind, j.strftime("%Y-%m-%d")]) + self.skipped_slices.append([grid_ind, j.strftime('%Y-%m-%d')]) else: - self.gridcenterlist.append([f'grid{grid_ind} ' + f'Lat:{str(self.gridpoints[grid_ind][1])} Lon:{str(self.gridpoints[grid_ind][0])}']) - lonarr = np.array( - grid_subset[grid_subset['Date'] == j]['Lon']) - latarr = np.array( - grid_subset[grid_subset['Date'] == j]['Lat']) - delayarray = np.array( - grid_subset[grid_subset['Date'] == j][self.col_name]) + self.gridcenterlist.append( + [ + f'grid{grid_ind} ' + + f'Lat:{str(self.gridpoints[grid_ind][1])} Lon:{str(self.gridpoints[grid_ind][0])}' + ] + ) + lonarr = np.array(grid_subset[grid_subset['Date'] == j]['Lon']) + latarr = np.array(grid_subset[grid_subset['Date'] == j]['Lat']) + delayarray = np.array(grid_subset[grid_subset['Date'] == j][self.col_name]) # fit empirical variogram for each time AND grid dists, vario = self._emp_vario(lonarr, latarr, delayarray) - dists_binned, vario_binned = self._binned_vario( - dists, vario) + dists_binned, vario_binned = self._binned_vario(dists, vario) # fit experimental variogram for each time AND grid, model default is exponential res_robust, d_test, v_test = self._fit_vario( - dists_binned, vario_binned, model=self.__exponential__, x0=None, Nparm=3) + dists_binned, vario_binned, model=self.__exponential__, x0=None, Nparm=3 + ) # Plot empirical + experimental variogram for this gridnode and timeslice if not os.path.exists(os.path.join(self.workdir, f'variograms/grid{grid_ind}')): - os.makedirs(os.path.join( - self.workdir, f'variograms/grid{grid_ind}')) + os.makedirs(os.path.join(self.workdir, f'variograms/grid{grid_ind}')) # Make variogram plots for each time-slice if self.variogram_per_timeslice: # Plot empirical variogram for this gridnode and timeslice - self.plot_variogram(grid_ind, j.strftime("%Y%m%d"), [self.gridpoints[grid_ind][1], self.gridpoints[grid_ind][0]], - workdir=os.path.join(self.workdir, f'variograms/grid{grid_ind}'), dists=dists, vario=vario, - dists_binned=dists_binned, vario_binned=vario_binned) + self.plot_variogram( + grid_ind, + j.strftime('%Y%m%d'), + [self.gridpoints[grid_ind][1], self.gridpoints[grid_ind][0]], + workdir=os.path.join(self.workdir, f'variograms/grid{grid_ind}'), + dists=dists, + vario=vario, + dists_binned=dists_binned, + vario_binned=vario_binned, + ) # Plot experimental variogram for this gridnode and timeslice - self.plot_variogram(grid_ind, j.strftime("%Y%m%d"), [self.gridpoints[grid_ind][1], self.gridpoints[grid_ind][0]], - workdir=os.path.join(self.workdir, f'variograms/grid{grid_ind}'), d_test=d_test, v_test=v_test, - res_robust=res_robust.x, dists_binned=dists_binned, vario_binned=vario_binned) + self.plot_variogram( + grid_ind, + j.strftime('%Y%m%d'), + [self.gridpoints[grid_ind][1], self.gridpoints[grid_ind][0]], + workdir=os.path.join(self.workdir, f'variograms/grid{grid_ind}'), + d_test=d_test, + v_test=v_test, + res_robust=res_robust.x, + dists_binned=dists_binned, + vario_binned=vario_binned, + ) # append for plotting - self.good_slices.append([grid_ind, j.strftime("%Y%m%d")]) + self.good_slices.append([grid_ind, j.strftime('%Y%m%d')]) dists_arr.append(dists) vario_arr.append(vario) dists_binned_arr.append(dists_binned) @@ -522,28 +796,45 @@ def _append_variogram(self, grid_ind, grid_subset): vario_binned_arr = np.concatenate(vario_binned_arr).ravel() else: # dists_binned_arr = dists_arr ; vario_binned_arr = vario_arr - dists_binned_arr, vario_binned_arr = self._binned_vario( - dists_arr, vario_arr) + dists_binned_arr, vario_binned_arr = self._binned_vario(dists_arr, vario_arr) TOT_res_robust, TOT_d_test, TOT_v_test = self._fit_vario( - dists_binned_arr, vario_binned_arr, model=self.__exponential__, x0=None, Nparm=3) + dists_binned_arr, vario_binned_arr, model=self.__exponential__, x0=None, Nparm=3 + ) tot_timetag = self.good_slices[0][1] + '–' + self.good_slices[-1][1] # Append TOT arrays self.TOT_good_slices.append([grid_ind, tot_timetag]) self.TOT_res_robust_arr.append(TOT_res_robust.x) self.TOT_tot_timetag.append(tot_timetag) - var_rmse = np.sqrt(np.nanmean((TOT_res_robust.fun)**2)) + var_rmse = np.sqrt(np.nanmean((TOT_res_robust.fun) ** 2)) if var_rmse <= self.variogram_errlimit: self.TOT_res_robust_rmse.append(var_rmse) else: self.TOT_res_robust_rmse.append(np.array(np.nan)) # Plot empirical variogram for this gridnode - self.plot_variogram(grid_ind, tot_timetag, [self.gridpoints[grid_ind][1], self.gridpoints[grid_ind][0]], - workdir=os.path.join(self.workdir, f'variograms/grid{grid_ind}'), dists=dists_arr, vario=vario_arr, - dists_binned=dists_binned_arr, vario_binned=vario_binned_arr, seasonalinterval=self.seasonalinterval) + self.plot_variogram( + grid_ind, + tot_timetag, + [self.gridpoints[grid_ind][1], self.gridpoints[grid_ind][0]], + workdir=os.path.join(self.workdir, f'variograms/grid{grid_ind}'), + dists=dists_arr, + vario=vario_arr, + dists_binned=dists_binned_arr, + vario_binned=vario_binned_arr, + seasonalinterval=self.seasonalinterval, + ) # Plot experimental variogram for this gridnode - self.plot_variogram(grid_ind, tot_timetag, [self.gridpoints[grid_ind][1], self.gridpoints[grid_ind][0]], - workdir=os.path.join(self.workdir, f'variograms/grid{grid_ind}'), d_test=TOT_d_test, v_test=TOT_v_test, - res_robust=TOT_res_robust.x, seasonalinterval=self.seasonalinterval, dists_binned=dists_binned_arr, vario_binned=vario_binned_arr) + self.plot_variogram( + grid_ind, + tot_timetag, + [self.gridpoints[grid_ind][1], self.gridpoints[grid_ind][0]], + workdir=os.path.join(self.workdir, f'variograms/grid{grid_ind}'), + d_test=TOT_d_test, + v_test=TOT_v_test, + res_robust=TOT_res_robust.x, + seasonalinterval=self.seasonalinterval, + dists_binned=dists_binned_arr, + vario_binned=vario_binned_arr, + ) # Record sparse grids which didn't have sufficient sample size of data through any of the timeslices else: self.sparse_grids.append(grid_ind) @@ -577,21 +868,33 @@ def create_variograms(self): self.gridcenterlist.extend(l) # save grid-center lookup table - self.gridcenterlist = [list(i) for i in set(tuple(j) - for j in self.gridcenterlist)] + self.gridcenterlist = [list(i) for i in set(tuple(j) for j in self.gridcenterlist)] self.gridcenterlist.sort(key=lambda x: int(x[0][4:6])) - gridcenter = open( - (os.path.join(self.workdir, 'variograms/gridlocation_lookup.txt')), "w") + gridcenter = open((os.path.join(self.workdir, 'variograms/gridlocation_lookup.txt')), 'w') for element in self.gridcenterlist: - gridcenter.writelines("\n".join(element)) - gridcenter.write("\n") + gridcenter.writelines('\n'.join(element)) + gridcenter.write('\n') gridcenter.close() TOT_grids = [i[0] for i in self.TOT_good_slices] return TOT_grids, self.TOT_res_robust_arr, self.TOT_res_robust_rmse - def plot_variogram(self, gridID, timeslice, coords, workdir='./', d_test=None, v_test=None, res_robust=None, dists=None, vario=None, dists_binned=None, vario_binned=None, seasonalinterval=None) -> None: + def plot_variogram( + self, + gridID, + timeslice, + coords, + workdir='./', + d_test=None, + v_test=None, + res_robust=None, + dists=None, + vario=None, + dists_binned=None, + vario_binned=None, + seasonalinterval=None, + ) -> None: """Make empirical and/or experimental variogram fit plots.""" # If specified workdir doesn't exist, create it if not os.path.exists(workdir): @@ -600,8 +903,10 @@ def plot_variogram(self, gridID, timeslice, coords, workdir='./', d_test=None, v # make plot title title_str = f' \nLat:{coords[1]:.2f} Lon:{coords[0]:.2f}\nTime:{str(timeslice)}' if seasonalinterval: - title_str += ' Season(mm/dd): {}/{} – {}/{}'.format(int(timeslice[4:6]), int( - timeslice[6:8]), int(timeslice[-4:-2]), int(timeslice[-2:])) + title_str += ( + ' Season(mm/dd): ' + f'{int(timeslice[4:6])}/{int(timeslice[6:8])} – {int(timeslice[-4:-2])}/{int(timeslice[-2:])}' + ) if dists is not None and vario is not None: # scale from m to user-defined units @@ -612,36 +917,29 @@ def plot_variogram(self, gridID, timeslice, coords, workdir='./', d_test=None, v dists_binned = [convert_SI(i, 'm', self.unit) for i in dists_binned] plt.plot(dists_binned, vario_binned, 'bo', label='binned') if res_robust is not None: - plt.axhline(y=res_robust[1], color='g', - linestyle='--', label=f'ɣ\u0332\u00b2({self.unit}\u00b2)') + plt.axhline(y=res_robust[1], color='g', linestyle='--', label=f'ɣ\u0332\u00b2({self.unit}\u00b2)') # scale from m to user-defined units res_robust[0] = convert_SI(res_robust[0], 'm', self.unit) - plt.axvline(x=res_robust[0], color='c', - linestyle='--', label=f'h ({self.unit})') + plt.axvline(x=res_robust[0], color='c', linestyle='--', label=f'h ({self.unit})') if d_test is not None and v_test is not None: # scale from m to user-defined units d_test = [convert_SI(i, 'm', self.unit) for i in d_test] plt.plot(d_test, v_test, 'r-', label='experimental fit') plt.xlabel(f'Distance ({self.unit})') plt.ylabel(f'Dissimilarity ({self.unit}\u00b2)') - plt.legend(bbox_to_anchor=(1.02, 1), - loc='upper left', borderaxespad=0., framealpha=1.) + plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0.0, framealpha=1.0) # Plot empirical variogram if d_test is None and v_test is None: plt.title('Empirical variogram' + title_str) plt.tight_layout() - plt.savefig(os.path.join( - workdir, f'grid{gridID}_timeslice{timeslice}_justEMPvariogram.eps')) + plt.savefig(os.path.join(workdir, f'grid{gridID}_timeslice{timeslice}_justEMPvariogram.eps')) # Plot just experimental variogram else: plt.title('Experimental variogram' + title_str) plt.tight_layout() - plt.savefig(os.path.join( - workdir, f'grid{gridID}_timeslice{timeslice}_justEXPvariogram.eps')) + plt.savefig(os.path.join(workdir, f'grid{gridID}_timeslice{timeslice}_justEXPvariogram.eps')) plt.close() - return - class RaiderStats: """Class which loads standard weather model/GPS delay files and generates a series of user-requested statistics and graphics.""" @@ -649,12 +947,38 @@ class RaiderStats: # import dependencies import glob - def __init__(self, filearg, col_name, unit='m', workdir='./', bbox=None, spacing=1, timeinterval=None, seasonalinterval=None, - obs_errlimit='inf', time_lines=False, stationsongrids=False, station_seasonal_phase=False, cbounds=None, colorpercentile=[25, 95], - usr_colormap='hot_r', grid_heatmap=False, grid_delay_mean=False, grid_delay_median=False, grid_delay_stdev=False, - grid_seasonal_phase=False, grid_delay_absolute_mean=False, grid_delay_absolute_median=False, - grid_delay_absolute_stdev=False, grid_seasonal_absolute_phase=False, grid_to_raster=False, min_span=[2, 0.6], - period_limit=0., numCPUs=8, phaseamp_per_station=False) -> None: + def __init__( + self, + filearg, + col_name, + unit='m', + workdir='./', + bbox=None, + spacing=1, + timeinterval=None, + seasonalinterval=None, + obs_errlimit='inf', + time_lines=False, + stationsongrids=False, + station_seasonal_phase=False, + cbounds=None, + colorpercentile=[25, 95], + usr_colormap='hot_r', + grid_heatmap=False, + grid_delay_mean=False, + grid_delay_median=False, + grid_delay_stdev=False, + grid_seasonal_phase=False, + grid_delay_absolute_mean=False, + grid_delay_absolute_median=False, + grid_delay_absolute_stdev=False, + grid_seasonal_absolute_phase=False, + grid_to_raster=False, + min_span=[2, 0.6], + period_limit=0.0, + numCPUs=8, + phaseamp_per_station=False, + ) -> None: self.fname = filearg self.col_name = col_name self.unit = unit @@ -712,81 +1036,251 @@ def __init__(self, filearg, col_name, unit='m', workdir='./', bbox=None, spacing if self.colorpercentile is None: self.colorpercentile = [25, 95] if self.colorpercentile[0] > self.colorpercentile[1]: - raise Exception(f'Input colorpercentile lower threshold {self.colorpercentile[0]} higher than upper threshold {self.colorpercentile[1]}') + raise Exception( + f'Input colorpercentile lower threshold {self.colorpercentile[0]} higher than upper threshold {self.colorpercentile[1]}' + ) # load dataframe directly if previously generated TIF grid-file if self.fname.endswith('.tif'): if 'grid_heatmap' in self.fname: - self.grid_heatmap, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_heatmap, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_heatmap')[0] if 'grid_delay_mean' in self.fname: - self.grid_delay_mean, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_delay_mean, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_delay_mean')[0] if 'grid_delay_median' in self.fname: - self.grid_delay_median, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_delay_median, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_delay_median')[0] if 'grid_delay_stdev' in self.fname: - self.grid_delay_stdev, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_delay_stdev, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_delay_stdev')[0] if 'grid_seasonal_phase' in self.fname: - self.grid_seasonal_phase, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_phase, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_phase')[0] if 'grid_seasonal_period' in self.fname: - self.grid_seasonal_period, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_period, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_period')[0] if 'grid_seasonal_amplitude' in self.fname: - self.grid_seasonal_amplitude, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_amplitude, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_amplitude')[0] if 'grid_seasonal_phase_stdev' in self.fname: - self.grid_seasonal_phase_stdev, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_phase_stdev, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_phase_stdev')[0] if 'grid_seasonal_amplitude_stdev' in self.fname: - self.grid_seasonal_amplitude_stdev, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_amplitude_stdev, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_amplitude_stdev')[0] if 'grid_seasonal_period_stdev' in self.fname: - self.grid_seasonal_period_stdev, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_period_stdev, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_period_stdev')[0] if 'grid_seasonal_fit_rmse' in self.fname: - self.grid_seasonal_fit_rmse, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_fit_rmse, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_fit_rmse')[0] if 'grid_delay_absolute_mean' in self.fname: - self.grid_delay_absolute_mean, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_delay_absolute_mean, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_delay_absolute_mean')[0] if 'grid_delay_absolute_median' in self.fname: - self.grid_delay_absolute_median, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_delay_absolute_median, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_delay_absolute_median')[0] if 'grid_delay_absolute_stdev' in self.fname: - self.grid_delay_absolute_stdev, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_delay_absolute_stdev, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_delay_absolute_stdev')[0] if 'grid_seasonal_absolute_phase' in self.fname: - self.grid_seasonal_absolute_phase, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_absolute_phase, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_absolute_phase')[0] if 'grid_seasonal_absolute_period' in self.fname: - self.grid_seasonal_absolute_period, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_absolute_period, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_absolute_period')[0] if 'grid_seasonal_absolute_amplitude' in self.fname: - self.grid_seasonal_absolute_amplitude, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_absolute_amplitude, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_absolute_amplitude')[0] if 'grid_seasonal_absolute_phase_stdev' in self.fname: - self.grid_seasonal_absolute_phase_stdev, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_absolute_phase_stdev, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_absolute_phase_stdev')[0] if 'grid_seasonal_absolute_amplitude_stdev' in self.fname: - self.grid_seasonal_absolute_amplitude_stdev, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_absolute_amplitude_stdev, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_absolute_amplitude_stdev')[0] if 'grid_seasonal_absolute_period_stdev' in self.fname: - self.grid_seasonal_absolute_period_stdev, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_absolute_period_stdev, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_absolute_period_stdev')[0] if 'grid_seasonal_absolute_fit_rmse' in self.fname: - self.grid_seasonal_absolute_fit_rmse, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_seasonal_absolute_fit_rmse, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_seasonal_absolute_fit_rmse')[0] if 'grid_range' in self.fname: - self.grid_range, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_range, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_range')[0] if 'grid_variance' in self.fname: - self.grid_variance, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_variance, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_variance')[0] if 'grid_variogram_rmse' in self.fname: - self.grid_variogram_rmse, self.plotbbox, self.spacing, self.colorbarfmt, self.stationsongrids, self.time_lines = load_gridfile(self.fname, self.unit) + ( + self.grid_variogram_rmse, + self.plotbbox, + self.spacing, + self.colorbarfmt, + self.stationsongrids, + self.time_lines, + ) = load_gridfile(self.fname, self.unit) self.col_name = os.path.basename(self.fname).split('_' + 'grid_variogram_rmse')[0] # setup dataframe for statistical analyses (if CSV) if self.fname.endswith('.csv'): @@ -794,34 +1288,57 @@ def __init__(self, filearg, col_name, unit='m', workdir='./', bbox=None, spacing def _get_extent(self): # dataset, spacing=1, userbbox=None """Get the bbox, spacing in deg (by default 1deg), optionally pass user-specified bbox. Output array in WESN degrees.""" - extent = [np.floor(min(self.df['Lon'])), np.ceil(max(self.df['Lon'])), - np.floor(min(self.df['Lat'])), np.ceil(max(self.df['Lat']))] + extent = [ + np.floor(min(self.df['Lon'])), + np.ceil(max(self.df['Lon'])), + np.floor(min(self.df['Lat'])), + np.ceil(max(self.df['Lat'])), + ] if self.bbox is not None: - dfextents_poly = Polygon(np.column_stack((np.array([extent[0], extent[0], extent[1], extent[1], extent[0]]), - np.array([extent[2], extent[3], extent[3], extent[2], extent[2]])))) - userbbox_poly = Polygon(np.column_stack((np.array([self.bbox[2], self.bbox[3], self.bbox[3], self.bbox[2], self.bbox[2]]), - np.array([self.bbox[0], self.bbox[0], self.bbox[1], self.bbox[1], self.bbox[0]])))) + dfextents_poly = Polygon( + np.column_stack( + ( + np.array([extent[0], extent[0], extent[1], extent[1], extent[0]]), + np.array([extent[2], extent[3], extent[3], extent[2], extent[2]]), + ) + ) + ) + userbbox_poly = Polygon( + np.column_stack( + ( + np.array([self.bbox[2], self.bbox[3], self.bbox[3], self.bbox[2], self.bbox[2]]), + np.array([self.bbox[0], self.bbox[0], self.bbox[1], self.bbox[1], self.bbox[0]]), + ) + ) + ) if userbbox_poly.intersects(dfextents_poly): extent = [np.floor(self.bbox[2]), np.ceil(self.bbox[-1]), np.floor(self.bbox[0]), np.ceil(self.bbox[1])] else: - raise Exception("User-specified bounds do not overlap with dataset bounds, adjust bounds and re-run program.") - if extent[0] < -180. or extent[1] > 180. or extent[2] < -90. or extent[3] > 90.: - raise Exception("Specified bounds exceed -180/180 lon and/or -90/90 lat, adjust bounds and re-run program.") + raise Exception( + 'User-specified bounds do not overlap with dataset bounds, adjust bounds and re-run program.' + ) + if extent[0] < -180.0 or extent[1] > 180.0 or extent[2] < -90.0 or extent[3] > 90.0: + raise Exception( + 'Specified bounds exceed -180/180 lon and/or -90/90 lat, adjust bounds and re-run program.' + ) del dfextents_poly, userbbox_poly # ensure that extents do not exceed -180/180 lon and -90/90 lat - if extent[0] < -180.: - extent[0] = -180. - if extent[1] > 180.: - extent[1] = 180. - if extent[2] < -90.: - extent[2] = -90. - if extent[3] > 90.: - extent[3] = 90. + if extent[0] < -180.0: + extent[0] = -180.0 + if extent[1] > 180.0: + extent[1] = 180.0 + if extent[2] < -90.0: + extent[2] = -90.0 + if extent[3] > 90.0: + extent[3] = 90.0 # ensure even spacing, set spacing to 1 if specified spacing is not even multiple of bounds if (extent[1] - extent[0]) % self.spacing != 0 or (extent[-1] - extent[-2]) % self.spacing: - logger.warning("User-specified spacing %s is not even multiple of bounds, resetting spacing to 1\N{DEGREE SIGN}", self.spacing) + logger.warning( + 'User-specified spacing %s is not even multiple of bounds, resetting spacing to 1\N{DEGREE SIGN}', + self.spacing, + ) self.spacing = 1 # Create corners of rectangle to be transformed to a grid @@ -829,8 +1346,7 @@ def _get_extent(self): # dataset, spacing=1, userbbox=None se = [extent[1] - (self.spacing / 2), extent[2] + (self.spacing / 2)] # Store grid dimension [y,x] - grid_dim = [int((extent[1] - extent[0]) / self.spacing), - int((extent[-1] - extent[-2]) / self.spacing)] + grid_dim = [int((extent[1] - extent[0]) / self.spacing), int((extent[-1] - extent[-2]) / self.spacing)] # Iterate over 2D area gridpoints = [] @@ -854,8 +1370,12 @@ def _check_stationgrid_intersection(self, stat_ID): Return index of grid cell which intersects with station Note: Fast, but assumes station locations don't change. """ - coord = Point((self.unique_points[1][self.unique_points[0].index( - stat_ID)], self.unique_points[2][self.unique_points[0].index(stat_ID)])) + coord = Point( + ( + self.unique_points[1][self.unique_points[0].index(stat_ID)], + self.unique_points[2][self.unique_points[0].index(stat_ID)], + ) + ) # Get grid cell polygon which intersect with station coordinate grid_int = self.polygon_tree.query(coord) # Pass corresponding grid cell index @@ -869,14 +1389,15 @@ def _reader(self): try: data = pd.read_csv(self.fname, parse_dates=['Datetime']) data['Date'] = data['Datetime'].apply(lambda x: x.date()) - data['Date'] = data['Date'].apply(lambda x: dt.datetime.strptime(x.strftime("%Y-%m-%d"), "%Y-%m-%d")) + data['Date'] = data['Date'].apply(lambda x: dt.datetime.strptime(x.strftime('%Y-%m-%d'), '%Y-%m-%d')) except BaseException: data = pd.read_csv(self.fname, parse_dates=['Date']) # check if user-specified key is valid if self.col_name not in data.keys(): raise Exception( - f'User-specified key {self.col_name} not found in input file {self.fname}. Must specify valid key.' ) + f'User-specified key {self.col_name} not found in input file {self.fname}. Must specify valid key.' + ) # if user-specified key is the same as the 'Date' field, rename if self.col_name == 'Date': @@ -910,46 +1431,56 @@ def create_DF(self) -> None: # time-interval filter if self.timeinterval: - self.timeinterval = [dt.datetime.strptime( - val, '%Y-%m-%d') for val in self.timeinterval.split()] - self.df = self.df[(self.df['Date'] >= self.timeinterval[0]) & ( - self.df['Date'] <= self.timeinterval[-1])] + self.timeinterval = [dt.datetime.strptime(val, '%Y-%m-%d') for val in self.timeinterval.split()] + self.df = self.df[(self.df['Date'] >= self.timeinterval[0]) & (self.df['Date'] <= self.timeinterval[-1])] # seasonal filter if self.seasonalinterval: self.seasonalinterval = self.seasonalinterval.split() # get day of year - self.seasonalinterval = [dt.datetime.strptime('2001-' + self.seasonalinterval[0], '%Y-%m-%d').timetuple( - ).tm_yday, dt.datetime.strptime('2001-' + self.seasonalinterval[-1], '%Y-%m-%d').timetuple().tm_yday] + self.seasonalinterval = [ + dt.datetime.strptime('2001-' + self.seasonalinterval[0], '%Y-%m-%d').timetuple().tm_yday, + dt.datetime.strptime('2001-' + self.seasonalinterval[-1], '%Y-%m-%d').timetuple().tm_yday, + ] # track input order and wrap around year if necessary # e.g. month/day: 03/01 to 06/01 if self.seasonalinterval[0] < self.seasonalinterval[1]: # non leap-year - filtered_self = self.df[(not self.df['Date'].dt.is_leap_year) & ( - self.df['Date'].dt.dayofyear >= self.seasonalinterval[0]) & (self.df['Date'].dt.dayofyear <= self.seasonalinterval[-1])] + filtered_self = self.df[ + (not self.df['Date'].dt.is_leap_year) + & (self.df['Date'].dt.dayofyear >= self.seasonalinterval[0]) + & (self.df['Date'].dt.dayofyear <= self.seasonalinterval[-1]) + ] # leap-year - self.seasonalinterval = [i + 1 if i > - 59 else i for i in self.seasonalinterval] - filtered_self_ly = self.df[(self.df['Date'].dt.is_leap_year) & ( - self.df['Date'].dt.dayofyear >= self.seasonalinterval[0]) & (self.df['Date'].dt.dayofyear <= self.seasonalinterval[-1])] + self.seasonalinterval = [i + 1 if i > 59 else i for i in self.seasonalinterval] + filtered_self_ly = self.df[ + (self.df['Date'].dt.is_leap_year) + & (self.df['Date'].dt.dayofyear >= self.seasonalinterval[0]) + & (self.df['Date'].dt.dayofyear <= self.seasonalinterval[-1]) + ] self.df = pd.concat([filtered_self, filtered_self_ly], ignore_index=True) del filtered_self # e.g. month/day: 12/01 to 03/01 if self.seasonalinterval[0] > self.seasonalinterval[1]: # non leap-year - filtered_self = self.df[(not self.df['Date'].dt.is_leap_year) & ( - self.df['Date'].dt.dayofyear >= self.seasonalinterval[-1]) & (self.df['Date'].dt.dayofyear <= self.seasonalinterval[0])] + filtered_self = self.df[ + (not self.df['Date'].dt.is_leap_year) + & (self.df['Date'].dt.dayofyear >= self.seasonalinterval[-1]) + & (self.df['Date'].dt.dayofyear <= self.seasonalinterval[0]) + ] # leap-year - self.seasonalinterval = [i + 1 if i > - 59 else i for i in self.seasonalinterval] - filtered_self_ly = self.df[(self.df['Date'].dt.is_leap_year) & ( - self.df['Date'].dt.dayofyear >= self.seasonalinterval[-1]) & (self.df['Date'].dt.dayofyear <= self.seasonalinterval[0])] + self.seasonalinterval = [i + 1 if i > 59 else i for i in self.seasonalinterval] + filtered_self_ly = self.df[ + (self.df['Date'].dt.is_leap_year) + & (self.df['Date'].dt.dayofyear >= self.seasonalinterval[-1]) + & (self.df['Date'].dt.dayofyear <= self.seasonalinterval[0]) + ] self.df = pd.concat([filtered_self, filtered_self_ly], ignore_index=True) del filtered_self # estimate central longitude lines if '--time_lines' specified if self.time_lines and 'Datetime' in self.df.keys(): - self.df['Date_hr'] = self.df['Datetime'].dt.hour.astype(float).astype("Int32") + self.df['Date_hr'] = self.df['Datetime'].dt.hour.astype(float).astype('Int32') # get list of unique times all_hrs = sorted(set(self.df['Date_hr'])) @@ -957,8 +1488,7 @@ def create_DF(self) -> None: central_points = [] # if single time, avoid loop if len(all_hrs) == 1: - central_points.append(([0, max(self.df['Lon'])], - [0, min(self.df['Lon'])])) + central_points.append(([0, max(self.df['Lon'])], [0, min(self.df['Lon'])])) else: for i in enumerate(all_hrs): # last entry @@ -968,10 +1498,10 @@ def create_DF(self) -> None: elif i[0] == 0: lons = self.df[self.df['Date_hr'] < all_hrs[i[0] + 1]] else: - lons = self.df[(self.df['Date_hr'] > all_hrs[i[0] - 1]) - & (self.df['Date_hr'] < all_hrs[i[0] + 1])] - central_points.append(([0, max(lons['Lon'])], - [0, min(lons['Lon'])])) + lons = self.df[ + (self.df['Date_hr'] > all_hrs[i[0] - 1]) & (self.df['Date_hr'] < all_hrs[i[0] + 1]) + ] + central_points.append(([0, max(lons['Lon'])], [0, min(lons['Lon'])])) # get central longitudes self.time_lines = [midpoint(i[0], i[1]) for i in central_points] @@ -982,22 +1512,38 @@ def create_DF(self) -> None: self.bbox = [float(val) for val in self.bbox.split()] except BaseException: raise Exception( - 'Cannot understand the --bounding_box argument. String input is incorrect or path does not exist.') + 'Cannot understand the --bounding_box argument. String input is incorrect or path does not exist.' + ) self.plotbbox, self.grid_dim, self.gridpoints = self._get_extent() # generate list of grid-polygons append_poly = [] for i in self.gridpoints: - bbox = [i[1] - (self.spacing / 2), i[1] + (self.spacing / 2), - i[0] - (self.spacing / 2), i[0] + (self.spacing / 2)] - append_poly.append(Polygon(np.column_stack((np.array([bbox[2], bbox[3], bbox[3], bbox[2], bbox[2]]), - np.array([bbox[0], bbox[0], bbox[1], bbox[1], bbox[0]]))))) # Pass lons/lats to create polygon + bbox = [ + i[1] - (self.spacing / 2), + i[1] + (self.spacing / 2), + i[0] - (self.spacing / 2), + i[0] + (self.spacing / 2), + ] + append_poly.append( + Polygon( + np.column_stack( + ( + np.array([bbox[2], bbox[3], bbox[3], bbox[2], bbox[2]]), + np.array([bbox[0], bbox[0], bbox[1], bbox[1], bbox[0]]), + ) + ) + ) + ) # Pass lons/lats to create polygon # Check for grid cell intersection with each station idtogrid_dict = {} self.unique_points = self.df.groupby(['ID', 'Lon', 'Lat']).size() - self.unique_points = [self.unique_points.index.get_level_values('ID').tolist(), self.unique_points.index.get_level_values( - 'Lon').tolist(), self.unique_points.index.get_level_values('Lat').tolist()] + self.unique_points = [ + self.unique_points.index.get_level_values('ID').tolist(), + self.unique_points.index.get_level_values('Lon').tolist(), + self.unique_points.index.get_level_values('Lat').tolist(), + ] # Initiate R-tree of gridded array domain self.polygon_tree = STRtree(append_poly) for stat_ID in self.unique_points[0]: @@ -1014,108 +1560,236 @@ def create_DF(self) -> None: # If specified, pass station locations to superimpose on gridplots if self.stationsongrids: unique_points = self.df.groupby(['Lon', 'Lat']).size() - self.stationsongrids = [unique_points.index.get_level_values( - 'Lon').tolist(), unique_points.index.get_level_values('Lat').tolist()] + self.stationsongrids = [ + unique_points.index.get_level_values('Lon').tolist(), + unique_points.index.get_level_values('Lat').tolist(), + ] # If specified, setup gridded array(s) if self.grid_heatmap: - self.grid_heatmap = np.array([np.nan if i[0] not in self.df['gridnode'].values[:] else int(len(np.unique( - self.df['ID'][self.df['gridnode'] == i[0]]))) for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_heatmap = ( + np.array( + [ + np.nan + if i[0] not in self.df['gridnode'].values[:] + else int(len(np.unique(self.df['ID'][self.df['gridnode'] == i[0]]))) + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_heatmap' + '.tif') - save_gridfile(self.grid_heatmap, 'grid_heatmap', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%1i', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='int16', - noData=0) + save_gridfile( + self.grid_heatmap, + 'grid_heatmap', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%1i', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='int16', + noData=0, + ) if self.grid_delay_mean: # Take mean of station-wise means per gridcell unique_points = self.df.groupby(['ID', 'Lon', 'Lat', 'gridnode'], as_index=False)[self.col_name].mean() unique_points = unique_points.groupby(['gridnode'])[self.col_name].mean() unique_points.dropna(how='any', inplace=True) - self.grid_delay_mean = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_delay_mean = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_delay_mean' + '.tif') - save_gridfile(self.grid_delay_mean, 'grid_delay_mean', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%.2f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + save_gridfile( + self.grid_delay_mean, + 'grid_delay_mean', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%.2f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) if self.grid_delay_median: # Take mean of station-wise medians per gridcell unique_points = self.df.groupby(['ID', 'Lon', 'Lat', 'gridnode'], as_index=False)[self.col_name].median() unique_points = unique_points.groupby(['gridnode'])[self.col_name].mean() unique_points.dropna(how='any', inplace=True) - self.grid_delay_median = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_delay_median = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_delay_median' + '.tif') - save_gridfile(self.grid_delay_median, 'grid_delay_median', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%.2f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + save_gridfile( + self.grid_delay_median, + 'grid_delay_median', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%.2f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) if self.grid_delay_stdev: # Take mean of station-wise stdev per gridcell unique_points = self.df.groupby(['ID', 'Lon', 'Lat', 'gridnode'], as_index=False)[self.col_name].std() unique_points = unique_points.groupby(['gridnode'])[self.col_name].mean() unique_points.dropna(how='any', inplace=True) - self.grid_delay_stdev = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_delay_stdev = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_delay_stdev' + '.tif') - save_gridfile(self.grid_delay_stdev, 'grid_delay_stdev', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%.2f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + save_gridfile( + self.grid_delay_stdev, + 'grid_delay_stdev', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%.2f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) if self.grid_delay_absolute_mean: # Take mean of all data per gridcell unique_points = self.df.groupby(['gridnode'])[self.col_name].mean() unique_points.dropna(how='any', inplace=True) - self.grid_delay_absolute_mean = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_delay_absolute_mean = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_delay_absolute_mean' + '.tif') - save_gridfile(self.grid_delay_absolute_mean, 'grid_delay_absolute_mean', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%.2f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + save_gridfile( + self.grid_delay_absolute_mean, + 'grid_delay_absolute_mean', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%.2f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) if self.grid_delay_absolute_median: # Take median of all data per gridcell unique_points = self.df.groupby(['gridnode'])[self.col_name].median() unique_points.dropna(how='any', inplace=True) - self.grid_delay_absolute_median = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_delay_absolute_median = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_delay_absolute_median' + '.tif') - save_gridfile(self.grid_delay_absolute_median, 'grid_delay_absolute_median', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%.2f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + save_gridfile( + self.grid_delay_absolute_median, + 'grid_delay_absolute_median', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%.2f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) if self.grid_delay_absolute_stdev: # Take stdev of all data per gridcell unique_points = self.df.groupby(['gridnode'])[self.col_name].std() unique_points.dropna(how='any', inplace=True) - self.grid_delay_absolute_stdev = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_delay_absolute_stdev = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_delay_absolute_stdev' + '.tif') - save_gridfile(self.grid_delay_absolute_stdev, 'grid_delay_absolute_stdev', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%.2f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + save_gridfile( + self.grid_delay_absolute_stdev, + 'grid_delay_absolute_stdev', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%.2f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) # If specified, compute phase/amplitude fits if self.station_seasonal_phase or self.grid_seasonal_phase or self.grid_seasonal_absolute_phase: @@ -1133,7 +1807,16 @@ def create_DF(self) -> None: args = [] for i in sorted(list(set(unique_points['ID']))): # pass all values corresponding to station (ID, data = y, time = x) - args.append((i, unique_points[unique_points['ID'] == i]['Date'].to_list(), unique_points[unique_points['ID'] == i][self.col_name].to_list(), self.min_span[0], self.min_span[1], self.period_limit)) + args.append( + ( + i, + unique_points[unique_points['ID'] == i]['Date'].to_list(), + unique_points[unique_points['ID'] == i][self.col_name].to_list(), + self.min_span[0], + self.min_span[1], + self.period_limit, + ) + ) # Parallelize iteration through all grid-cells and time slices with multiprocessing.Pool(self.numCPUs) as multipool: for i, j, k, l, m, n, o in multipool.starmap(self._amplitude_and_phase, args): @@ -1151,7 +1834,9 @@ def create_DF(self) -> None: self.df['phsfit'] = self.df['ID'].map(self.phsfit) # check if there are any valid data values if self.df['phsfit'].isnull().values.all(axis=0): - raise Exception(f"No valid data values, adjust --min_span inputs for time span in years {self.min_span[0]} and/or fractional obs. {self.min_span[1]}") + raise Exception( + f'No valid data values, adjust --min_span inputs for time span in years {self.min_span[0]} and/or fractional obs. {self.min_span[1]}' + ) self.df['ampfit'] = self.df['ID'].map(self.ampfit) self.df['periodfit'] = self.df['ID'].map(self.periodfit) self.phsfit_c = {k: v for d in self.phsfit_c for k, v in d.items()} @@ -1170,184 +1855,458 @@ def create_DF(self) -> None: unique_points = self.df.groupby(['ID', 'Lon', 'Lat', 'gridnode'], as_index=False)['phsfit'].mean() unique_points = unique_points.groupby(['gridnode'])['phsfit'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_phase = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_phase = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_phase' + '.tif') - save_gridfile(self.grid_seasonal_phase, 'grid_seasonal_phase', gridfile_name, self.plotbbox, self.spacing, - 'days', colorbarfmt='%.1i', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + save_gridfile( + self.grid_seasonal_phase, + 'grid_seasonal_phase', + gridfile_name, + self.plotbbox, + self.spacing, + 'days', + colorbarfmt='%.1i', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) # Pass mean amplitude of station-wise means per gridcell unique_points = self.df.groupby(['ID', 'Lon', 'Lat', 'gridnode'], as_index=False)['ampfit'].mean() unique_points = unique_points.groupby(['gridnode'])['ampfit'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_amplitude = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_amplitude = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_amplitude' + '.tif') - save_gridfile(self.grid_seasonal_amplitude, 'grid_seasonal_amplitude', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%.3f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + save_gridfile( + self.grid_seasonal_amplitude, + 'grid_seasonal_amplitude', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%.3f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) # Pass mean period of station-wise means per gridcell unique_points = self.df.groupby(['ID', 'Lon', 'Lat', 'gridnode'], as_index=False)['periodfit'].mean() unique_points = unique_points.groupby(['gridnode'])['periodfit'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_period = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_period = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_period' + '.tif') - save_gridfile(self.grid_seasonal_period, 'grid_seasonal_period', gridfile_name, self.plotbbox, self.spacing, - 'years', colorbarfmt='%.2f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + save_gridfile( + self.grid_seasonal_period, + 'grid_seasonal_period', + gridfile_name, + self.plotbbox, + self.spacing, + 'years', + colorbarfmt='%.2f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) ######################################################################################################################## # Pass mean phase stdev of station-wise means per gridcell unique_points = self.df.groupby(['ID', 'Lon', 'Lat', 'gridnode'], as_index=False)['phsfit_c'].mean() unique_points = unique_points.groupby(['gridnode'])['phsfit_c'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_phase_stdev = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_phase_stdev = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: - gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_phase_stdev' + '.tif') - save_gridfile(self.grid_seasonal_phase_stdev, 'grid_seasonal_phase_stdev', gridfile_name, self.plotbbox, self.spacing, - 'days', colorbarfmt='%.1i', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + gridfile_name = os.path.join( + self.workdir, self.col_name + '_' + 'grid_seasonal_phase_stdev' + '.tif' + ) + save_gridfile( + self.grid_seasonal_phase_stdev, + 'grid_seasonal_phase_stdev', + gridfile_name, + self.plotbbox, + self.spacing, + 'days', + colorbarfmt='%.1i', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) # Pass mean amplitude stdev of station-wise means per gridcell unique_points = self.df.groupby(['ID', 'Lon', 'Lat', 'gridnode'], as_index=False)['ampfit_c'].mean() unique_points = unique_points.groupby(['gridnode'])['ampfit_c'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_amplitude_stdev = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_amplitude_stdev = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: - gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_amplitude_stdev' + '.tif') - save_gridfile(self.grid_seasonal_amplitude_stdev, 'grid_seasonal_amplitude_stdev', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%.3f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + gridfile_name = os.path.join( + self.workdir, self.col_name + '_' + 'grid_seasonal_amplitude_stdev' + '.tif' + ) + save_gridfile( + self.grid_seasonal_amplitude_stdev, + 'grid_seasonal_amplitude_stdev', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%.3f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) # Pass mean period stdev of station-wise means per gridcell unique_points = self.df.groupby(['ID', 'Lon', 'Lat', 'gridnode'], as_index=False)['periodfit_c'].mean() unique_points = unique_points.groupby(['gridnode'])['periodfit_c'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_period_stdev = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_period_stdev = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: - gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_period_stdev' + '.tif') - save_gridfile(self.grid_seasonal_period_stdev, 'grid_seasonal_period_stdev', gridfile_name, self.plotbbox, self.spacing, - 'years', colorbarfmt='%.2e', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + gridfile_name = os.path.join( + self.workdir, self.col_name + '_' + 'grid_seasonal_period_stdev' + '.tif' + ) + save_gridfile( + self.grid_seasonal_period_stdev, + 'grid_seasonal_period_stdev', + gridfile_name, + self.plotbbox, + self.spacing, + 'years', + colorbarfmt='%.2e', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) # Pass mean seasonal fit RMSE of station-wise means per gridcell - unique_points = self.df.groupby(['ID', 'Lon', 'Lat', 'gridnode'], as_index=False)['seasonalfit_rmse'].mean() + unique_points = self.df.groupby(['ID', 'Lon', 'Lat', 'gridnode'], as_index=False)[ + 'seasonalfit_rmse' + ].mean() unique_points = unique_points.groupby(['gridnode'])['seasonalfit_rmse'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_fit_rmse = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_fit_rmse = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_fit_rmse' + '.tif') - save_gridfile(self.grid_seasonal_fit_rmse, 'grid_seasonal_fit_rmse', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%.3f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + save_gridfile( + self.grid_seasonal_fit_rmse, + 'grid_seasonal_fit_rmse', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%.3f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) ######################################################################################################################## if self.grid_seasonal_absolute_phase: # Pass absolute mean phase of all data per gridcell unique_points = self.df.groupby(['gridnode'])['phsfit'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_absolute_phase = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_absolute_phase = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: - gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_phase' + '.tif') - save_gridfile(self.grid_seasonal_absolute_phase, 'grid_seasonal_absolute_phase', gridfile_name, self.plotbbox, self.spacing, - 'days', colorbarfmt='%.1i', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + gridfile_name = os.path.join( + self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_phase' + '.tif' + ) + save_gridfile( + self.grid_seasonal_absolute_phase, + 'grid_seasonal_absolute_phase', + gridfile_name, + self.plotbbox, + self.spacing, + 'days', + colorbarfmt='%.1i', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) # Pass absolute mean amplitude of all data per gridcell unique_points = self.df.groupby(['gridnode'])['ampfit'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_absolute_amplitude = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_absolute_amplitude = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: - gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_amplitude' + '.tif') - save_gridfile(self.grid_seasonal_absolute_amplitude, 'grid_seasonal_absolute_amplitude', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%.3f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + gridfile_name = os.path.join( + self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_amplitude' + '.tif' + ) + save_gridfile( + self.grid_seasonal_absolute_amplitude, + 'grid_seasonal_absolute_amplitude', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%.3f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) # Pass absolute mean period of all data per gridcell unique_points = self.df.groupby(['gridnode'])['periodfit'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_absolute_period = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_absolute_period = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: - gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_period' + '.tif') - save_gridfile(self.grid_seasonal_absolute_period, 'grid_seasonal_absolute_period', gridfile_name, self.plotbbox, self.spacing, - 'years', colorbarfmt='%.2f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + gridfile_name = os.path.join( + self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_period' + '.tif' + ) + save_gridfile( + self.grid_seasonal_absolute_period, + 'grid_seasonal_absolute_period', + gridfile_name, + self.plotbbox, + self.spacing, + 'years', + colorbarfmt='%.2f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) ######################################################################################################################## # Pass absolute mean phase stdev of all data per gridcell unique_points = self.df.groupby(['gridnode'])['phsfit_c'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_absolute_phase_stdev = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_absolute_phase_stdev = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: - gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_phase_stdev' + '.tif') - save_gridfile(self.grid_seasonal_absolute_phase_stdev, 'grid_seasonal_absolute_phase_stdev', gridfile_name, self.plotbbox, self.spacing, - 'days', colorbarfmt='%.1i', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + gridfile_name = os.path.join( + self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_phase_stdev' + '.tif' + ) + save_gridfile( + self.grid_seasonal_absolute_phase_stdev, + 'grid_seasonal_absolute_phase_stdev', + gridfile_name, + self.plotbbox, + self.spacing, + 'days', + colorbarfmt='%.1i', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) # Pass absolute mean amplitude stdev of all data per gridcell unique_points = self.df.groupby(['gridnode'])['ampfit_c'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_absolute_amplitude_stdev = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_absolute_amplitude_stdev = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: - gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_amplitude_stdev' + '.tif') - save_gridfile(self.grid_seasonal_absolute_amplitude_stdev, 'grid_seasonal_absolute_amplitude_stdev', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%.3f', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + gridfile_name = os.path.join( + self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_amplitude_stdev' + '.tif' + ) + save_gridfile( + self.grid_seasonal_absolute_amplitude_stdev, + 'grid_seasonal_absolute_amplitude_stdev', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%.3f', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) # Pass absolute mean period stdev of all data per gridcell unique_points = self.df.groupby(['gridnode'])['periodfit_c'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_absolute_period_stdev = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_absolute_period_stdev = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: - gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_period_stdev' + '.tif') - save_gridfile(self.grid_seasonal_absolute_period_stdev, 'grid_seasonal_absolute_period_stdev', gridfile_name, self.plotbbox, self.spacing, - 'years', colorbarfmt='%.2e', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') + gridfile_name = os.path.join( + self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_period_stdev' + '.tif' + ) + save_gridfile( + self.grid_seasonal_absolute_period_stdev, + 'grid_seasonal_absolute_period_stdev', + gridfile_name, + self.plotbbox, + self.spacing, + 'years', + colorbarfmt='%.2e', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) # Pass absolute mean seasonal fit RMSE of all data per gridcell unique_points = self.df.groupby(['gridnode'])['seasonalfit_rmse'].mean() unique_points.dropna(how='any', inplace=True) - self.grid_seasonal_absolute_fit_rmse = np.array([np.nan if i[0] not in unique_points.index.get_level_values('gridnode').tolist( - ) else unique_points[i[0]] for i in enumerate(self.gridpoints)]).reshape(self.grid_dim).T + self.grid_seasonal_absolute_fit_rmse = ( + np.array( + [ + np.nan + if i[0] not in unique_points.index.get_level_values('gridnode').tolist() + else unique_points[i[0]] + for i in enumerate(self.gridpoints) + ] + ) + .reshape(self.grid_dim) + .T + ) # If specified, save gridded array(s) if self.grid_to_raster: - gridfile_name = os.path.join(self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_fit_rmse' + '.tif') - save_gridfile(self.grid_seasonal_absolute_fit_rmse, 'grid_seasonal_absolute_fit_rmse', gridfile_name, self.plotbbox, self.spacing, - self.unit, colorbarfmt='%.2e', - stationsongrids=self.stationsongrids, - time_lines=self.time_lines, dtype='float32') - - def _amplitude_and_phase(self, station, tt, yy, min_span=2, min_frac=0.6, period_limit=0.): + gridfile_name = os.path.join( + self.workdir, self.col_name + '_' + 'grid_seasonal_absolute_fit_rmse' + '.tif' + ) + save_gridfile( + self.grid_seasonal_absolute_fit_rmse, + 'grid_seasonal_absolute_fit_rmse', + gridfile_name, + self.plotbbox, + self.spacing, + self.unit, + colorbarfmt='%.2e', + stationsongrids=self.stationsongrids, + time_lines=self.time_lines, + dtype='float32', + ) + + def _amplitude_and_phase(self, station, tt, yy, min_span=2, min_frac=0.6, period_limit=0.0): """ Fit sin to the input time sequence, and return fitting parameters: "amp", "omega", "phase", "offset", "freq", "period" and "fitfunc". @@ -1370,15 +2329,17 @@ def _amplitude_and_phase(self, station, tt, yy, min_span=2, min_frac=0.6, period periodfit_c[station] = np.nan seasonalfit_rmse[station] = np.nan # Fit with custom fit function with fixed period, if specified - if period_limit != 0.: + if period_limit != 0.0: # convert from years to radians/seconds - w = (1 / period_limit) * (1 / 31556952) * (2. * np.pi) + w = (1 / period_limit) * (1 / 31556952) * (2.0 * np.pi) def custom_sine_function_base(t, A, p, c): return self._sine_function_base(t, A, w, p, c) else: + def custom_sine_function_base(t, A, w, p, c): return self._sine_function_base(t, A, w, p, c) + # If station TS does not span specified time period, pass NaNs time_span_yrs = (max(tt) - min(tt)) / 31556952 if time_span_yrs >= min_span and len(list(set(tt))) / (time_span_yrs * 365.25) >= min_frac: @@ -1387,16 +2348,16 @@ def custom_sine_function_base(t, A, w, p, c): ff = np.fft.fftfreq(len(tt), (tt[1] - tt[0])) # assume uniform spacing Fyy = abs(np.fft.fft(yy)) guess_freq = abs(ff[np.argmax(Fyy[1:]) + 1]) # excluding the zero period "peak", which is related to offset - guess_amp = np.std(yy) * 2.**0.5 + guess_amp = np.std(yy) * 2.0**0.5 guess_offset = np.mean(yy) - guess = np.array([guess_amp, 2. * np.pi * guess_freq, 0., guess_offset]) + guess = np.array([guess_amp, 2.0 * np.pi * guess_freq, 0.0, guess_offset]) # Adjust frequency guess to reflect fixed period, if specified - if period_limit != 0.: - guess = np.array([guess_amp, 0., guess_offset]) + if period_limit != 0.0: + guess = np.array([guess_amp, 0.0, guess_offset]) # Catch warning where covariance cannot be estimated # I.e. OptimizeWarning: Covariance of the parameters could not be estimated with warnings.catch_warnings(): - warnings.simplefilter("error", OptimizeWarning) + warnings.simplefilter('error', OptimizeWarning) try: optimize_warning = False try: @@ -1404,29 +2365,46 @@ def custom_sine_function_base(t, A, w, p, c): popt, pcov = optimize.curve_fit(custom_sine_function_base, tt, yy, p0=guess, maxfev=int(1e6)) # If sparse input such that fittitng is not possible, pass NaNs except TypeError: - self.ampfit.append(np.nan), self.phsfit.append(np.nan), self.periodfit.append(np.nan), \ - self.ampfit_c.append(np.nan), self.phsfit_c.append(np.nan), \ - self.periodfit_c.append(np.nan), self.seasonalfit_rmse.append(np.nan) - return self.ampfit, self.phsfit, self.periodfit, self.ampfit_c, \ - self.phsfit_c, self.periodfit_c, self.seasonalfit_rmse + ( + self.ampfit.append(np.nan), + self.phsfit.append(np.nan), + self.periodfit.append(np.nan), + self.ampfit_c.append(np.nan), + self.phsfit_c.append(np.nan), + self.periodfit_c.append(np.nan), + self.seasonalfit_rmse.append(np.nan), + ) + return ( + self.ampfit, + self.phsfit, + self.periodfit, + self.ampfit_c, + self.phsfit_c, + self.periodfit_c, + self.seasonalfit_rmse, + ) except OptimizeWarning: optimize_warning = True - warnings.simplefilter("ignore", OptimizeWarning) + warnings.simplefilter('ignore', OptimizeWarning) popt, pcov = optimize.curve_fit(custom_sine_function_base, tt, yy, p0=guess, maxfev=int(1e6)) - print('OptimizeWarning: Covariance for station {} could not be estimated. Refer to debug figure here {} \ - '.format(station, os.path.join(self.workdir, 'phaseamp_per_station', f'station{station}.png'))) + debug_figure_path = os.path.join(self.workdir, 'phaseamp_per_station', f'station{station}.png') + print( + f'OptimizeWarning: Covariance for station {station} could not be estimated. ' + f'Refer to debug figure here {debug_figure_path}' + ) pass # Adjust expected output to reflect fixed period, if specified - if period_limit != 0.: + if period_limit != 0.0: A, p, c = popt else: A, w, p, c = popt # convert from radians/seconds to years - f = (w / (2. * np.pi)) * (31556952) + f = (w / (2.0 * np.pi)) * (31556952) f = 1 / f def fitfunc(t): return A * np.sin(w * t + p) + c + # Outputs = "amp": A, "angular frequency": w, "phase": p, "offset": c, "freq": f, "period": 1./f, # "fitfunc": fitfunc, "maxcov": np.max(pcov), "rawres": (guess,popt,pcov) # Pass amplitude (specified units) and phase (days) and stdev @@ -1441,13 +2419,14 @@ def fitfunc(t): with np.errstate(invalid='raise'): try: # pass covariance for each parameter - ampfit_c[station] = pcov[0, 0]**0.5 - periodfit_c[station] = pcov[1, 1]**0.5 - phsfit_c[station] = pcov[2, 2]**0.5 + ampfit_c[station] = pcov[0, 0] ** 0.5 + periodfit_c[station] = pcov[1, 1] ** 0.5 + phsfit_c[station] = pcov[2, 2] ** 0.5 # pass RMSE of fit seasonalfit_rmse[station] = yy - custom_sine_function_base(tt, *popt) - seasonalfit_rmse[station] = (np.sum(seasonalfit_rmse[station]**2) / - (seasonalfit_rmse[station].size - 2))**0.5 + seasonalfit_rmse[station] = ( + np.sum(seasonalfit_rmse[station] ** 2) / (seasonalfit_rmse[station].size - 2) + ) ** 0.5 except FloatingPointError: pass if self.phaseamp_per_station or optimize_warning: @@ -1456,9 +2435,9 @@ def fitfunc(t): tt_plot = copy.deepcopy(tt) tt_plot -= min(tt_plot) tt_plot /= 31556952 - plt.plot(tt_plot, yy, "ok", label="input") - plt.xlabel("time (years)") - plt.ylabel(f"data ({self.unit})") + plt.plot(tt_plot, yy, 'ok', label='input') + plt.xlabel('time (years)') + plt.ylabel(f'data ({self.unit})') num_testpoints = len(tt) * 10 if num_testpoints > 1000: num_testpoints = 1000 @@ -1467,12 +2446,15 @@ def fitfunc(t): tt2_plot = copy.deepcopy(tt2) tt2_plot -= min(tt2_plot) tt2_plot /= 31556952 - plt.plot(tt2_plot, fitfunc(tt2), "r-", label="fit", linewidth=2) - plt.legend(loc="best") + plt.plot(tt2_plot, fitfunc(tt2), 'r-', label='fit', linewidth=2) + plt.legend(loc='best') if not os.path.exists(os.path.join(self.workdir, 'phaseamp_per_station')): os.mkdir(os.path.join(self.workdir, 'phaseamp_per_station')) - plt.savefig(os.path.join(self.workdir, 'phaseamp_per_station', f'station{station}.png'), - format='png', bbox_inches='tight') + plt.savefig( + os.path.join(self.workdir, 'phaseamp_per_station', f'station{station}.png'), + format='png', + bbox_inches='tight', + ) plt.close() optimize_warning = False @@ -1484,14 +2466,32 @@ def fitfunc(t): self.periodfit_c.append(periodfit_c) self.seasonalfit_rmse.append(seasonalfit_rmse) - return self.ampfit, self.phsfit, self.periodfit, self.ampfit_c, \ - self.phsfit_c, self.periodfit_c, self.seasonalfit_rmse + return ( + self.ampfit, + self.phsfit, + self.periodfit, + self.ampfit_c, + self.phsfit_c, + self.periodfit_c, + self.seasonalfit_rmse, + ) def _sine_function_base(self, t, A, w, p, c): """Base function for modeling sinusoidal amplitude/phase fits.""" return A * np.sin(w * t + p) + c - def __call__(self, gridarr, plottype, workdir='./', drawgridlines=False, colorbarfmt='%.2f', stationsongrids=None, resValue=5, plotFormat='pdf', userTitle=None): + def __call__( + self, + gridarr, + plottype, + workdir='./', + drawgridlines=False, + colorbarfmt='%.2f', + stationsongrids=None, + resValue=5, + plotFormat='pdf', + userTitle=None, + ): """Visualize a suite of statistics w.r.t. stations. Pass either a list of points or a gridded array as the first argument. Alternatively, you may superimpose your gridded array with a supplementary list of points by passing the latter through the stationsongrids argument.""" from cartopy import crs as ccrs from cartopy import feature as cfeature @@ -1513,53 +2513,46 @@ def __call__(self, gridarr, plottype, workdir='./', drawgridlines=False, colorba fig, axes = plt.subplots(subplot_kw={'projection': ccrs.PlateCarree()}) # by default set background to white - axes.add_feature(cfeature.NaturalEarthFeature( - 'physical', 'land', '50m', facecolor='white'), zorder=0) + axes.add_feature(cfeature.NaturalEarthFeature('physical', 'land', '50m', facecolor='white'), zorder=0) axes.set_extent(self.plotbbox, ccrs.PlateCarree()) # add coastlines - axes.coastlines(linewidth=0.2, color="gray", zorder=4) + axes.coastlines(linewidth=0.2, color='gray', zorder=4) cmap = copy.copy(mpl.cm.get_cmap(self.usr_colormap)) # cmap.set_bad('black', 0.) # extract all colors from the hot map cmaplist = [cmap(i) for i in range(cmap.N)] # create the new map - cmap = mpl.colors.LinearSegmentedColormap.from_list( - 'Custom cmap', cmaplist) + cmap = mpl.colors.LinearSegmentedColormap.from_list('Custom cmap', cmaplist) axes.set_xlabel('Longitude', weight='bold', zorder=2) axes.set_ylabel('Latitude', weight='bold', zorder=2) # set ticks - axes.set_xticks(np.linspace( - self.plotbbox[0], self.plotbbox[1], 5), crs=ccrs.PlateCarree()) - axes.set_yticks(np.linspace( - self.plotbbox[2], self.plotbbox[3], 5), crs=ccrs.PlateCarree()) - lon_formatter = LongitudeFormatter( - number_format='.0f', degree_symbol='') - lat_formatter = LatitudeFormatter( - number_format='.0f', degree_symbol='') + axes.set_xticks(np.linspace(self.plotbbox[0], self.plotbbox[1], 5), crs=ccrs.PlateCarree()) + axes.set_yticks(np.linspace(self.plotbbox[2], self.plotbbox[3], 5), crs=ccrs.PlateCarree()) + lon_formatter = LongitudeFormatter(number_format='.0f', degree_symbol='') + lat_formatter = LatitudeFormatter(number_format='.0f', degree_symbol='') axes.xaxis.set_major_formatter(lon_formatter) axes.yaxis.set_major_formatter(lat_formatter) # draw central longitude lines corresponding to respective datetimes if self.time_lines: - tl = axes.grid(axis='x', linewidth=1.5, - color='blue', alpha=0.5, linestyle='-', - zorder=3) + tl = axes.grid(axis='x', linewidth=1.5, color='blue', alpha=0.5, linestyle='-', zorder=3) # If individual stations passed if isinstance(gridarr, list): # spatial distribution of stations - if plottype == "station_distribution": - im = axes.scatter(gridarr[0], gridarr[1], zorder=1, s=0.5, - marker='.', color='b', transform=ccrs.PlateCarree()) + if plottype == 'station_distribution': + im = axes.scatter( + gridarr[0], gridarr[1], zorder=1, s=0.5, marker='.', color='b', transform=ccrs.PlateCarree() + ) # passing 3rd column as z-value if len(gridarr) > 2: # set land/water background to light gray/blue respectively so station point data can be seen - axes.add_feature(cfeature.NaturalEarthFeature( - 'physical', 'land', '50m', facecolor='#A9A9A9'), zorder=0) - axes.add_feature(cfeature.NaturalEarthFeature( - 'physical', 'ocean', '50m', facecolor='#ADD8E6'), zorder=0) + axes.add_feature(cfeature.NaturalEarthFeature('physical', 'land', '50m', facecolor='#A9A9A9'), zorder=0) + axes.add_feature( + cfeature.NaturalEarthFeature('physical', 'ocean', '50m', facecolor='#ADD8E6'), zorder=0 + ) # set masked values as nans zvalues = gridarr[2] for i in nodat_arr: @@ -1568,15 +2561,18 @@ def __call__(self, gridarr, plottype, workdir='./', drawgridlines=False, colorba # define the bins and normalize if cbounds is None: # avoid "ufunc 'isnan'" error by casting array as float - cbounds = [np.nanpercentile(zvalues.astype('float'), self.colorpercentile[0]), np.nanpercentile( - zvalues.astype('float'), self.colorpercentile[1])] + cbounds = [ + np.nanpercentile(zvalues.astype('float'), self.colorpercentile[0]), + np.nanpercentile(zvalues.astype('float'), self.colorpercentile[1]), + ] # if upper/lower bounds identical, overwrite lower bound as 75% of upper bound to avoid plotting ValueError if cbounds[0] == cbounds[1]: cbounds[0] *= 0.75 cbounds.sort() # adjust precision for colorbar if necessary - if (abs(np.nanmax(zvalues) - np.nanmin(zvalues)) < 1 and (np.nanmean(zvalues)) < 1) \ - or abs(np.nanmax(zvalues) - np.nanmin(zvalues)) > 500: + if (abs(np.nanmax(zvalues) - np.nanmin(zvalues)) < 1 and (np.nanmean(zvalues)) < 1) or abs( + np.nanmax(zvalues) - np.nanmin(zvalues) + ) > 500: colorbarfmt = '%.2e' colorbounds = np.linspace(cbounds[0], cbounds[1], 256) @@ -1585,13 +2581,29 @@ def __call__(self, gridarr, plottype, workdir='./', drawgridlines=False, colorba colorbounds_ticks = np.linspace(cbounds[0], cbounds[1], 10) # plot data and initiate colorbar - im = axes.scatter(gridarr[0], gridarr[1], c=zvalues, cmap=cmap, norm=norm, - zorder=1, s=0.5, marker='.', transform=ccrs.PlateCarree()) + im = axes.scatter( + gridarr[0], + gridarr[1], + c=zvalues, + cmap=cmap, + norm=norm, + zorder=1, + s=0.5, + marker='.', + transform=ccrs.PlateCarree(), + ) # initiate colorbar and control height of colorbar divider = make_axes_locatable(axes) - cax = divider.append_axes("right", size="5%", pad=0.05, axes_class=plt.Axes) - cbar_ax = fig.colorbar(im, spacing='proportional', - ticks=colorbounds_ticks, boundaries=colorbounds, format=colorbarfmt, pad=0.1, cax=cax) + cax = divider.append_axes('right', size='5%', pad=0.05, axes_class=plt.Axes) + cbar_ax = fig.colorbar( + im, + spacing='proportional', + ticks=colorbounds_ticks, + boundaries=colorbounds, + format=colorbarfmt, + pad=0.1, + cax=cax, + ) cbar_ax.ax.minorticks_off() # If gridded area passed @@ -1601,21 +2613,22 @@ def __call__(self, gridarr, plottype, workdir='./', drawgridlines=False, colorba gridarr = np.ma.masked_where(gridarr == i, gridarr) gridarr = np.ma.filled(gridarr, np.nan) # set land/water background to light gray/blue respectively so grid cells can be seen - axes.add_feature(cfeature.NaturalEarthFeature( - 'physical', 'land', '50m', facecolor='#A9A9A9'), zorder=0) - axes.add_feature(cfeature.NaturalEarthFeature( - 'physical', 'ocean', '50m', facecolor='#ADD8E6'), zorder=0) + axes.add_feature(cfeature.NaturalEarthFeature('physical', 'land', '50m', facecolor='#A9A9A9'), zorder=0) + axes.add_feature(cfeature.NaturalEarthFeature('physical', 'ocean', '50m', facecolor='#ADD8E6'), zorder=0) # define the bins and normalize if cbounds is None: - cbounds = [np.nanpercentile(gridarr, self.colorpercentile[0]), np.nanpercentile( - gridarr, self.colorpercentile[1])] + cbounds = [ + np.nanpercentile(gridarr, self.colorpercentile[0]), + np.nanpercentile(gridarr, self.colorpercentile[1]), + ] # if upper/lower bounds identical, overwrite lower bound as 75% of upper bound to avoid plotting ValueError if cbounds[0] == cbounds[1]: cbounds[0] *= 0.75 cbounds.sort() # plot data and initiate colorbar - if (abs(np.nanmax(gridarr) - np.nanmin(gridarr)) < 1 and abs(np.nanmean(gridarr)) < 1) \ - or abs(np.nanmax(gridarr) - np.nanmin(gridarr)) > 500: + if (abs(np.nanmax(gridarr) - np.nanmin(gridarr)) < 1 and abs(np.nanmean(gridarr)) < 1) or abs( + np.nanmax(gridarr) - np.nanmin(gridarr) + ) > 500: colorbarfmt = '%.2e' colorbounds = np.linspace(cbounds[0], cbounds[1], 256) @@ -1624,65 +2637,127 @@ def __call__(self, gridarr, plottype, workdir='./', drawgridlines=False, colorba colorbounds_ticks = np.linspace(cbounds[0], cbounds[1], 10) # plot data - im = axes.imshow(gridarr, cmap=cmap, norm=norm, extent=self.plotbbox, - zorder=1, origin='upper', transform=ccrs.PlateCarree()) + im = axes.imshow( + gridarr, + cmap=cmap, + norm=norm, + extent=self.plotbbox, + zorder=1, + origin='upper', + transform=ccrs.PlateCarree(), + ) # initiate colorbar and control height of colorbar divider = make_axes_locatable(axes) - cax = divider.append_axes("right", size="5%", pad=0.05, axes_class=plt.Axes) - cbar_ax = fig.colorbar(im, spacing='proportional', ticks=colorbounds_ticks, - boundaries=colorbounds, format=colorbarfmt, pad=0.1, cax=cax) + cax = divider.append_axes('right', size='5%', pad=0.05, axes_class=plt.Axes) + cbar_ax = fig.colorbar( + im, + spacing='proportional', + ticks=colorbounds_ticks, + boundaries=colorbounds, + format=colorbarfmt, + pad=0.1, + cax=cax, + ) cbar_ax.ax.minorticks_off() # superimpose your gridded array with a supplementary list of point, if specified if self.stationsongrids: - axes.scatter(self.stationsongrids[0], self.stationsongrids[1], zorder=2, - s=0.5, marker='.', color='b', transform=ccrs.PlateCarree()) + axes.scatter( + self.stationsongrids[0], + self.stationsongrids[1], + zorder=2, + s=0.5, + marker='.', + color='b', + transform=ccrs.PlateCarree(), + ) # draw gridlines, if specified if drawgridlines: - gl = axes.gridlines(crs=ccrs.PlateCarree( - ), linewidth=0.5, color='black', alpha=0.5, linestyle='-', zorder=3) - gl.xlocator = mticker.FixedLocator(np.arange( - self.plotbbox[0], self.plotbbox[1] + self.spacing, self.spacing).tolist()) - gl.ylocator = mticker.FixedLocator(np.arange( - self.plotbbox[2], self.plotbbox[3] + self.spacing, self.spacing).tolist()) + gl = axes.gridlines( + crs=ccrs.PlateCarree(), linewidth=0.5, color='black', alpha=0.5, linestyle='-', zorder=3 + ) + gl.xlocator = mticker.FixedLocator( + np.arange(self.plotbbox[0], self.plotbbox[1] + self.spacing, self.spacing).tolist() + ) + gl.ylocator = mticker.FixedLocator( + np.arange(self.plotbbox[2], self.plotbbox[3] + self.spacing, self.spacing).tolist() + ) # Add labels to colorbar, if necessary if 'cbar_ax' in locals(): # experimental variogram fit sill heatmap - if plottype == "grid_variance": - cbar_ax.set_label(" ".join(plottype.replace('grid_', '').split('_')).title() + f' ({self.unit}\u00b2)', - rotation=-90, labelpad=10) + if plottype == 'grid_variance': + cbar_ax.set_label( + ' '.join(plottype.replace('grid_', '').split('_')).title() + f' ({self.unit}\u00b2)', + rotation=-90, + labelpad=10, + ) # specify appropriate units for mean/median/std/amplitude/experimental variogram fit heatmap - elif plottype == "grid_delay_mean" or plottype == "grid_delay_median" or plottype == "grid_delay_stdev" or \ - plottype == "grid_seasonal_amplitude" or plottype == "grid_range" or plottype == "station_delay_mean" or \ - plottype == "station_delay_median" or plottype == "station_delay_stdev" or \ - plottype == "station_seasonal_amplitude" or plottype == "grid_delay_absolute_mean" or \ - plottype == "grid_delay_absolute_median" or plottype == "grid_delay_absolute_stdev" or \ - plottype == "grid_seasonal_absolute_amplitude" or plottype == "grid_seasonal_amplitude_stdev" or \ - plottype == "grid_seasonal_absolute_amplitude_stdev" or plottype == "grid_seasonal_fit_rmse" or \ - plottype == "grid_seasonal_absolute_fit_rmse" or plottype == "grid_variogram_rmse": + elif ( + plottype == 'grid_delay_mean' + or plottype == 'grid_delay_median' + or plottype == 'grid_delay_stdev' + or plottype == 'grid_seasonal_amplitude' + or plottype == 'grid_range' + or plottype == 'station_delay_mean' + or plottype == 'station_delay_median' + or plottype == 'station_delay_stdev' + or plottype == 'station_seasonal_amplitude' + or plottype == 'grid_delay_absolute_mean' + or plottype == 'grid_delay_absolute_median' + or plottype == 'grid_delay_absolute_stdev' + or plottype == 'grid_seasonal_absolute_amplitude' + or plottype == 'grid_seasonal_amplitude_stdev' + or plottype == 'grid_seasonal_absolute_amplitude_stdev' + or plottype == 'grid_seasonal_fit_rmse' + or plottype == 'grid_seasonal_absolute_fit_rmse' + or plottype == 'grid_variogram_rmse' + ): # update label if sigZTD if 'sig' in self.col_name: - cbar_ax.set_label("sig ZTD " + " ".join(plottype.replace('grid_', - '').replace('delay_', '').split('_')).title() + f' ({self.unit})', - rotation=-90, labelpad=10) + cbar_ax.set_label( + 'sig ZTD ' + + ' '.join(plottype.replace('grid_', '').replace('delay_', '').split('_')).title() + + f' ({self.unit})', + rotation=-90, + labelpad=10, + ) else: - cbar_ax.set_label(" ".join(plottype.replace('grid_', '').split('_')).title() + f' ({self.unit})', - rotation=-90, labelpad=10) + cbar_ax.set_label( + ' '.join(plottype.replace('grid_', '').split('_')).title() + f' ({self.unit})', + rotation=-90, + labelpad=10, + ) # specify appropriate units for phase heatmap (days) - elif plottype == "station_seasonal_phase" or plottype == "grid_seasonal_phase" or plottype == "grid_seasonal_absolute_phase" or \ - plottype == "grid_seasonal_absolute_phase_stdev" or plottype == "grid_seasonal_phase_stdev": - cbar_ax.set_label(" ".join(plottype.replace('grid_', '').split('_')).title() + ' ({})'.format('days'), - rotation=-90, labelpad=10) + elif ( + plottype == 'station_seasonal_phase' + or plottype == 'grid_seasonal_phase' + or plottype == 'grid_seasonal_absolute_phase' + or plottype == 'grid_seasonal_absolute_phase_stdev' + or plottype == 'grid_seasonal_phase_stdev' + ): + cbar_ax.set_label( + ' '.join(plottype.replace('grid_', '').split('_')).title() + ' (days)', + rotation=-90, + labelpad=10, + ) # specify appropriate units for period heatmap (years) - elif plottype == "station_delay_period" or plottype == "grid_seasonal_period" or plottype == "grid_seasonal_absolute_period" or \ - plottype == "grid_seasonal_absolute_period_stdev" or plottype == "grid_seasonal_period_stdev": - cbar_ax.set_label(" ".join(plottype.replace('grid_', '').split('_')).title() + ' ({})'.format('years'), - rotation=-90, labelpad=10) + elif ( + plottype == 'station_delay_period' + or plottype == 'grid_seasonal_period' + or plottype == 'grid_seasonal_absolute_period' + or plottype == 'grid_seasonal_absolute_period_stdev' + or plottype == 'grid_seasonal_period_stdev' + ): + cbar_ax.set_label( + ' '.join(plottype.replace('grid_', '').split('_')).title() + ' (years)', + rotation=-90, + labelpad=10, + ) # gridmap of station density has no units else: - cbar_ax.set_label(" ".join(plottype.replace('grid_', '').split('_')).title(), rotation=-90, labelpad=10) + cbar_ax.set_label(' '.join(plottype.replace('grid_', '').split('_')).title(), rotation=-90, labelpad=10) # Add title to plots, if specified if userTitle: @@ -1691,13 +2766,14 @@ def __call__(self, gridarr, plottype, workdir='./', drawgridlines=False, colorba # save/close figure # cbar_ax.ax.locator_params(nbins=10) # for label in cbar_ax.ax.xaxis.get_ticklabels()[::25]: - # label.set_visible(False) - plt.savefig(os.path.join(workdir, self.col_name + '_' + plottype + '.' + plotFormat), - format=plotFormat, bbox_inches='tight') + # label.set_visible(False) + plt.savefig( + os.path.join(workdir, self.col_name + '_' + plottype + '.' + plotFormat), + format=plotFormat, + bbox_inches='tight', + ) plt.close() - return - def stats_analyses( fname, @@ -1743,7 +2819,7 @@ def stats_analyses( variogramplot, binnedvariogram, variogram_per_timeslice, - variogram_errlimit + variogram_errlimit, ) -> None: """ Main workflow for generating a suite of plots to illustrate spatiotemporal distribution @@ -1774,237 +2850,573 @@ def stats_analyses( grid_seasonal_absolute_phase = True variogramplot = True - logger.info("***Stats Function:***") + logger.info('***Stats Function:***') # prep dataframe object for plotting/variogram analysis based off of user specifications - df_stats = RaiderStats(fname, col_name, unit, workdir, bbox, spacing, - timeinterval, seasonalinterval, obs_errlimit, time_lines, stationsongrids, station_seasonal_phase, cbounds, colorpercentile, - usr_colormap, grid_heatmap, grid_delay_mean, grid_delay_median, grid_delay_stdev, grid_seasonal_phase, - grid_delay_absolute_mean, grid_delay_absolute_median, grid_delay_absolute_stdev, - grid_seasonal_absolute_phase, grid_to_raster, min_span, period_limit, numCPUs, phaseamp_per_station) + df_stats = RaiderStats( + fname, + col_name, + unit, + workdir, + bbox, + spacing, + timeinterval, + seasonalinterval, + obs_errlimit, + time_lines, + stationsongrids, + station_seasonal_phase, + cbounds, + colorpercentile, + usr_colormap, + grid_heatmap, + grid_delay_mean, + grid_delay_median, + grid_delay_stdev, + grid_seasonal_phase, + grid_delay_absolute_mean, + grid_delay_absolute_median, + grid_delay_absolute_stdev, + grid_seasonal_absolute_phase, + grid_to_raster, + min_span, + period_limit, + numCPUs, + phaseamp_per_station, + ) # Station plots # Plot each individual station if station_distribution: - logger.info("- Plot spatial distribution of stations.") + logger.info('- Plot spatial distribution of stations.') unique_points = df_stats.df.groupby(['Lon', 'Lat']).size() - df_stats([unique_points.index.get_level_values('Lon').tolist(), unique_points.index.get_level_values('Lat').tolist( - )], 'station_distribution', workdir=os.path.join(workdir, 'figures'), plotFormat=plot_fmt, userTitle=user_title) + df_stats( + [ + unique_points.index.get_level_values('Lon').tolist(), + unique_points.index.get_level_values('Lat').tolist(), + ], + 'station_distribution', + workdir=os.path.join(workdir, 'figures'), + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot mean delay per station if station_delay_mean: - logger.info("- Plot mean delay for each station.") - unique_points = df_stats.df.groupby( - ['Lon', 'Lat'])[col_name].median() + logger.info('- Plot mean delay for each station.') + unique_points = df_stats.df.groupby(['Lon', 'Lat'])[col_name].median() unique_points.dropna(how='any', inplace=True) - df_stats([unique_points.index.get_level_values('Lon').tolist(), unique_points.index.get_level_values('Lat').tolist( - ), unique_points.values], 'station_delay_mean', workdir=os.path.join(workdir, 'figures'), plotFormat=plot_fmt, userTitle=user_title) + df_stats( + [ + unique_points.index.get_level_values('Lon').tolist(), + unique_points.index.get_level_values('Lat').tolist(), + unique_points.values, + ], + 'station_delay_mean', + workdir=os.path.join(workdir, 'figures'), + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot median delay per station if station_delay_median: - logger.info("- Plot median delay for each station.") - unique_points = df_stats.df.groupby( - ['Lon', 'Lat'])[col_name].mean() + logger.info('- Plot median delay for each station.') + unique_points = df_stats.df.groupby(['Lon', 'Lat'])[col_name].mean() unique_points.dropna(how='any', inplace=True) - df_stats([unique_points.index.get_level_values('Lon').tolist(), unique_points.index.get_level_values('Lat').tolist( - ), unique_points.values], 'station_delay_median', workdir=os.path.join(workdir, 'figures'), plotFormat=plot_fmt, userTitle=user_title) + df_stats( + [ + unique_points.index.get_level_values('Lon').tolist(), + unique_points.index.get_level_values('Lat').tolist(), + unique_points.values, + ], + 'station_delay_median', + workdir=os.path.join(workdir, 'figures'), + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot delay stdev per station if station_delay_stdev: - logger.info("- Plot delay stdev for each station.") - unique_points = df_stats.df.groupby( - ['Lon', 'Lat'])[col_name].std() + logger.info('- Plot delay stdev for each station.') + unique_points = df_stats.df.groupby(['Lon', 'Lat'])[col_name].std() unique_points.dropna(how='any', inplace=True) - df_stats([unique_points.index.get_level_values('Lon').tolist(), unique_points.index.get_level_values('Lat').tolist( - ), unique_points.values], 'station_delay_stdev', workdir=os.path.join(workdir, 'figures'), plotFormat=plot_fmt, userTitle=user_title) + df_stats( + [ + unique_points.index.get_level_values('Lon').tolist(), + unique_points.index.get_level_values('Lat').tolist(), + unique_points.values, + ], + 'station_delay_stdev', + workdir=os.path.join(workdir, 'figures'), + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot delay phase/amplitude per station if station_seasonal_phase: - logger.info("- Plot delay phase/amplitude for each station.") + logger.info('- Plot delay phase/amplitude for each station.') # phase - unique_points_phase = df_stats.df.groupby( - ['Lon', 'Lat'])['phsfit'].mean() + unique_points_phase = df_stats.df.groupby(['Lon', 'Lat'])['phsfit'].mean() unique_points_phase.dropna(how='any', inplace=True) - df_stats([unique_points_phase.index.get_level_values('Lon').tolist(), unique_points_phase.index.get_level_values('Lat').tolist( - ), unique_points_phase.values], 'station_seasonal_phase', workdir=os.path.join(workdir, 'figures'), - colorbarfmt='%.1i', plotFormat=plot_fmt, userTitle=user_title) + df_stats( + [ + unique_points_phase.index.get_level_values('Lon').tolist(), + unique_points_phase.index.get_level_values('Lat').tolist(), + unique_points_phase.values, + ], + 'station_seasonal_phase', + workdir=os.path.join(workdir, 'figures'), + colorbarfmt='%.1i', + plotFormat=plot_fmt, + userTitle=user_title, + ) # amplitude - unique_points_amplitude = df_stats.df.groupby( - ['Lon', 'Lat'])['ampfit'].mean() + unique_points_amplitude = df_stats.df.groupby(['Lon', 'Lat'])['ampfit'].mean() unique_points_amplitude.dropna(how='any', inplace=True) - df_stats([unique_points_amplitude.index.get_level_values('Lon').tolist(), unique_points_amplitude.index.get_level_values('Lat').tolist( - ), unique_points_amplitude.values], 'station_seasonal_amplitude', workdir=os.path.join(workdir, 'figures'), - colorbarfmt='%.3f', plotFormat=plot_fmt, userTitle=user_title) + df_stats( + [ + unique_points_amplitude.index.get_level_values('Lon').tolist(), + unique_points_amplitude.index.get_level_values('Lat').tolist(), + unique_points_amplitude.values, + ], + 'station_seasonal_amplitude', + workdir=os.path.join(workdir, 'figures'), + colorbarfmt='%.3f', + plotFormat=plot_fmt, + userTitle=user_title, + ) # period - unique_points_period = df_stats.df.groupby( - ['Lon', 'Lat'])['periodfit'].mean() - df_stats([unique_points_period.index.get_level_values('Lon').tolist(), unique_points_period.index.get_level_values('Lat').tolist( - ), unique_points_period.values], 'station_delay_period', workdir=os.path.join(workdir, 'figures'), - colorbarfmt='%.2f', plotFormat=plot_fmt, userTitle=user_title) + unique_points_period = df_stats.df.groupby(['Lon', 'Lat'])['periodfit'].mean() + df_stats( + [ + unique_points_period.index.get_level_values('Lon').tolist(), + unique_points_period.index.get_level_values('Lat').tolist(), + unique_points_period.values, + ], + 'station_delay_period', + workdir=os.path.join(workdir, 'figures'), + colorbarfmt='%.2f', + plotFormat=plot_fmt, + userTitle=user_title, + ) # Gridded station plots # Plot density of stations for each gridcell if isinstance(df_stats.grid_heatmap, np.ndarray): - logger.info("- Plot density of stations per gridcell.") - df_stats(df_stats.grid_heatmap, 'grid_heatmap', workdir=os.path.join(workdir, 'figures'), drawgridlines=drawgridlines, - colorbarfmt='%.1i', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot density of stations per gridcell.') + df_stats( + df_stats.grid_heatmap, + 'grid_heatmap', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.1i', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot mean of station-wise mean delay across each gridcell if isinstance(df_stats.grid_delay_mean, np.ndarray): - logger.info("- Plot mean of station-wise mean delay across each gridcell.") - df_stats(df_stats.grid_delay_mean, 'grid_delay_mean', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.2f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot mean of station-wise mean delay across each gridcell.') + df_stats( + df_stats.grid_delay_mean, + 'grid_delay_mean', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.2f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot mean of station-wise median delay across each gridcell if isinstance(df_stats.grid_delay_median, np.ndarray): - logger.info("- Plot mean of station-wise median delay across each gridcell.") - df_stats(df_stats.grid_delay_median, 'grid_delay_median', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.2f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot mean of station-wise median delay across each gridcell.') + df_stats( + df_stats.grid_delay_median, + 'grid_delay_median', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.2f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot mean of station-wise stdev delay across each gridcell if isinstance(df_stats.grid_delay_stdev, np.ndarray): - logger.info("- Plot mean of station-wise stdev delay across each gridcell.") - df_stats(df_stats.grid_delay_stdev, 'grid_delay_stdev', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.2f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot mean of station-wise stdev delay across each gridcell.') + df_stats( + df_stats.grid_delay_stdev, + 'grid_delay_stdev', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.2f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot mean of station-wise delay phase across each gridcell if isinstance(df_stats.grid_seasonal_phase, np.ndarray): - logger.info("- Plot mean of station-wise delay phase across each gridcell.") - df_stats(df_stats.grid_seasonal_phase, 'grid_seasonal_phase', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.1i', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot mean of station-wise delay phase across each gridcell.') + df_stats( + df_stats.grid_seasonal_phase, + 'grid_seasonal_phase', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.1i', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot mean of station-wise delay amplitude across each gridcell if isinstance(df_stats.grid_seasonal_amplitude, np.ndarray): - logger.info("- Plot mean of station-wise delay amplitude across each gridcell.") - df_stats(df_stats.grid_seasonal_amplitude, 'grid_seasonal_amplitude', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.3f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot mean of station-wise delay amplitude across each gridcell.') + df_stats( + df_stats.grid_seasonal_amplitude, + 'grid_seasonal_amplitude', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.3f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot mean of station-wise delay period across each gridcell if isinstance(df_stats.grid_seasonal_period, np.ndarray): - logger.info("- Plot mean of station-wise delay period across each gridcell.") - df_stats(df_stats.grid_seasonal_period, 'grid_seasonal_period', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.2f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot mean of station-wise delay period across each gridcell.') + df_stats( + df_stats.grid_seasonal_period, + 'grid_seasonal_period', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.2f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot mean stdev of station-wise delay phase across each gridcell if isinstance(df_stats.grid_seasonal_phase_stdev, np.ndarray): - logger.info("- Plot mean stdev of station-wise delay phase across each gridcell.") - df_stats(df_stats.grid_seasonal_phase_stdev, 'grid_seasonal_phase_stdev', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.1i', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot mean stdev of station-wise delay phase across each gridcell.') + df_stats( + df_stats.grid_seasonal_phase_stdev, + 'grid_seasonal_phase_stdev', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.1i', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot mean stdev of station-wise delay amplitude across each gridcell if isinstance(df_stats.grid_seasonal_amplitude_stdev, np.ndarray): - logger.info("- Plot mean stdev of station-wise delay amplitude across each gridcell.") - df_stats(df_stats.grid_seasonal_amplitude_stdev, 'grid_seasonal_amplitude_stdev', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.3f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot mean stdev of station-wise delay amplitude across each gridcell.') + df_stats( + df_stats.grid_seasonal_amplitude_stdev, + 'grid_seasonal_amplitude_stdev', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.3f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot mean stdev of station-wise delay period across each gridcell if isinstance(df_stats.grid_seasonal_period_stdev, np.ndarray): - logger.info("- Plot mean stdev of station-wise delay period across each gridcell.") - df_stats(df_stats.grid_seasonal_period_stdev, 'grid_seasonal_period_stdev', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.2e', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot mean stdev of station-wise delay period across each gridcell.') + df_stats( + df_stats.grid_seasonal_period_stdev, + 'grid_seasonal_period_stdev', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.2e', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot mean of seasonal fit RMSE across each gridcell if isinstance(df_stats.grid_seasonal_fit_rmse, np.ndarray): - logger.info("- Plot mean of seasonal fit RMSE across each gridcell.") - df_stats(df_stats.grid_seasonal_fit_rmse, 'grid_seasonal_fit_rmse', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.3f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot mean of seasonal fit RMSE across each gridcell.') + df_stats( + df_stats.grid_seasonal_fit_rmse, + 'grid_seasonal_fit_rmse', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.3f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot absolute mean delay for each gridcell if isinstance(df_stats.grid_delay_absolute_mean, np.ndarray): - logger.info("- Plot absolute mean delay per gridcell.") - df_stats(df_stats.grid_delay_absolute_mean, 'grid_delay_absolute_mean', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.2f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot absolute mean delay per gridcell.') + df_stats( + df_stats.grid_delay_absolute_mean, + 'grid_delay_absolute_mean', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.2f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot absolute median delay for each gridcell if isinstance(df_stats.grid_delay_absolute_median, np.ndarray): - logger.info("- Plot absolute median delay per gridcell.") - df_stats(df_stats.grid_delay_absolute_median, 'grid_delay_absolute_median', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.2f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot absolute median delay per gridcell.') + df_stats( + df_stats.grid_delay_absolute_median, + 'grid_delay_absolute_median', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.2f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot absolute stdev delay for each gridcell if isinstance(df_stats.grid_delay_absolute_stdev, np.ndarray): - logger.info("- Plot absolute delay stdev per gridcell.") - df_stats(df_stats.grid_delay_absolute_stdev, 'grid_delay_absolute_stdev', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.2f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot absolute delay stdev per gridcell.') + df_stats( + df_stats.grid_delay_absolute_stdev, + 'grid_delay_absolute_stdev', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.2f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot absolute delay phase for each gridcell if isinstance(df_stats.grid_seasonal_absolute_phase, np.ndarray): - logger.info("- Plot absolute delay phase per gridcell.") - df_stats(df_stats.grid_seasonal_absolute_phase, 'grid_seasonal_absolute_phase', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.1i', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot absolute delay phase per gridcell.') + df_stats( + df_stats.grid_seasonal_absolute_phase, + 'grid_seasonal_absolute_phase', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.1i', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot absolute delay amplitude for each gridcell if isinstance(df_stats.grid_seasonal_absolute_amplitude, np.ndarray): - logger.info("- Plot absolute delay amplitude per gridcell.") - df_stats(df_stats.grid_seasonal_absolute_amplitude, 'grid_seasonal_absolute_amplitude', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.3f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot absolute delay amplitude per gridcell.') + df_stats( + df_stats.grid_seasonal_absolute_amplitude, + 'grid_seasonal_absolute_amplitude', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.3f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot absolute delay period for each gridcell if isinstance(df_stats.grid_seasonal_absolute_period, np.ndarray): - logger.info("- Plot absolute delay period per gridcell.") - df_stats(df_stats.grid_seasonal_absolute_period, 'grid_seasonal_absolute_period', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.2f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot absolute delay period per gridcell.') + df_stats( + df_stats.grid_seasonal_absolute_period, + 'grid_seasonal_absolute_period', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.2f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot absolute delay phase stdev for each gridcell if isinstance(df_stats.grid_seasonal_absolute_phase_stdev, np.ndarray): - logger.info("- Plot absolute delay phase stdev per gridcell.") - df_stats(df_stats.grid_seasonal_absolute_phase_stdev, 'grid_seasonal_absolute_phase_stdev', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.1i', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot absolute delay phase stdev per gridcell.') + df_stats( + df_stats.grid_seasonal_absolute_phase_stdev, + 'grid_seasonal_absolute_phase_stdev', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.1i', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot absolute delay amplitude stdev for each gridcell if isinstance(df_stats.grid_seasonal_absolute_amplitude_stdev, np.ndarray): - logger.info("- Plot absolute delay amplitude stdev per gridcell.") - df_stats(df_stats.grid_seasonal_absolute_amplitude_stdev, 'grid_seasonal_absolute_amplitude_stdev', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.3f', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot absolute delay amplitude stdev per gridcell.') + df_stats( + df_stats.grid_seasonal_absolute_amplitude_stdev, + 'grid_seasonal_absolute_amplitude_stdev', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.3f', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot absolute delay period stdev for each gridcell if isinstance(df_stats.grid_seasonal_absolute_period_stdev, np.ndarray): - logger.info("- Plot absolute delay period stdev per gridcell.") - df_stats(df_stats.grid_seasonal_absolute_period_stdev, 'grid_seasonal_absolute_period_stdev', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.2e', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot absolute delay period stdev per gridcell.') + df_stats( + df_stats.grid_seasonal_absolute_period_stdev, + 'grid_seasonal_absolute_period_stdev', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.2e', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Plot absolute mean seasonal fit RMSE for each gridcell if isinstance(df_stats.grid_seasonal_absolute_fit_rmse, np.ndarray): - logger.info("- Plot absolute mean seasonal fit RMSE per gridcell.") - df_stats(df_stats.grid_seasonal_absolute_fit_rmse, 'grid_seasonal_absolute_fit_rmse', workdir=os.path.join(workdir, 'figures'), - drawgridlines=drawgridlines, colorbarfmt='%.2e', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot absolute mean seasonal fit RMSE per gridcell.') + df_stats( + df_stats.grid_seasonal_absolute_fit_rmse, + 'grid_seasonal_absolute_fit_rmse', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.2e', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) # Perform variogram analysis - if variogramplot and not isinstance(df_stats.grid_range, np.ndarray) \ - and not isinstance(df_stats.grid_variance, np.ndarray) \ - and not isinstance(df_stats.grid_variogram_rmse, np.ndarray): - logger.info("***Variogram Analysis Function:***") + if ( + variogramplot + and not isinstance(df_stats.grid_range, np.ndarray) + and not isinstance(df_stats.grid_variance, np.ndarray) + and not isinstance(df_stats.grid_variogram_rmse, np.ndarray) + ): + logger.info('***Variogram Analysis Function:***') if unit in ['minute', 'hour', 'day', 'year']: unit = 'm' df_stats.unit = 'm' - logger.warning(f"Output unit {unit} specified for Variogram analysis. Reverted to meters") - make_variograms = VariogramAnalysis(df_stats.df, df_stats.gridpoints, col_name, unit, workdir, - df_stats.seasonalinterval, densitythreshold, binnedvariogram, - numCPUs, variogram_per_timeslice, variogram_errlimit) + logger.warning(f'Output unit {unit} specified for Variogram analysis. Reverted to meters') + make_variograms = VariogramAnalysis( + df_stats.df, + df_stats.gridpoints, + col_name, + unit, + workdir, + df_stats.seasonalinterval, + densitythreshold, + binnedvariogram, + numCPUs, + variogram_per_timeslice, + variogram_errlimit, + ) TOT_grids, TOT_res_robust_arr, TOT_res_robust_rmse = make_variograms.create_variograms() # get range - df_stats.grid_range = np.array([np.nan if i[0] not in TOT_grids else float(TOT_res_robust_arr[TOT_grids.index( - i[0])][0]) for i in enumerate(df_stats.gridpoints)]).reshape(df_stats.grid_dim).T + df_stats.grid_range = ( + np.array( + [ + np.nan if i[0] not in TOT_grids else float(TOT_res_robust_arr[TOT_grids.index(i[0])][0]) + for i in enumerate(df_stats.gridpoints) + ] + ) + .reshape(df_stats.grid_dim) + .T + ) # convert range to specified output unit df_stats.grid_range = convert_SI(df_stats.grid_range, 'm', unit) # get sill - df_stats.grid_variance = np.array([np.nan if i[0] not in TOT_grids else float(TOT_res_robust_arr[TOT_grids.index( - i[0])][1]) for i in enumerate(df_stats.gridpoints)]).reshape(df_stats.grid_dim).T + df_stats.grid_variance = ( + np.array( + [ + np.nan if i[0] not in TOT_grids else float(TOT_res_robust_arr[TOT_grids.index(i[0])][1]) + for i in enumerate(df_stats.gridpoints) + ] + ) + .reshape(df_stats.grid_dim) + .T + ) # convert sill to specified output unit df_stats.grid_range = convert_SI(df_stats.grid_range, 'm^2', unit.split('^2')[0] + '^2') # get variogram rmse - df_stats.grid_variogram_rmse = np.array([np.nan if i[0] not in TOT_grids else float(TOT_res_robust_rmse[TOT_grids.index( - i[0])]) for i in enumerate(df_stats.gridpoints)]).reshape(df_stats.grid_dim).T + df_stats.grid_variogram_rmse = ( + np.array( + [ + np.nan if i[0] not in TOT_grids else float(TOT_res_robust_rmse[TOT_grids.index(i[0])]) + for i in enumerate(df_stats.gridpoints) + ] + ) + .reshape(df_stats.grid_dim) + .T + ) # convert range to specified output unit df_stats.grid_variogram_rmse = convert_SI(df_stats.grid_variogram_rmse, 'm', unit) # If specified, save gridded array(s) if grid_to_raster: # write range gridfile_name = os.path.join(workdir, col_name + '_' + 'grid_range' + '.tif') - save_gridfile(df_stats.grid_range, 'grid_range', gridfile_name, df_stats.plotbbox, df_stats.spacing, - df_stats.unit, colorbarfmt='%1i', - stationsongrids=df_stats.stationsongrids, dtype='float32') + save_gridfile( + df_stats.grid_range, + 'grid_range', + gridfile_name, + df_stats.plotbbox, + df_stats.spacing, + df_stats.unit, + colorbarfmt='%1i', + stationsongrids=df_stats.stationsongrids, + dtype='float32', + ) # write sill gridfile_name = os.path.join(workdir, col_name + '_' + 'grid_variance' + '.tif') - save_gridfile(df_stats.grid_variance, 'grid_variance', gridfile_name, df_stats.plotbbox, df_stats.spacing, - df_stats.unit + '^2', colorbarfmt='%.3e', - stationsongrids=df_stats.stationsongrids, dtype='float32') + save_gridfile( + df_stats.grid_variance, + 'grid_variance', + gridfile_name, + df_stats.plotbbox, + df_stats.spacing, + df_stats.unit + '^2', + colorbarfmt='%.3e', + stationsongrids=df_stats.stationsongrids, + dtype='float32', + ) # write variogram rmse gridfile_name = os.path.join(workdir, col_name + '_' + 'grid_variogram_rmse' + '.tif') - save_gridfile(df_stats.grid_variogram_rmse, 'grid_variogram_rmse', gridfile_name, df_stats.plotbbox, df_stats.spacing, - df_stats.unit, colorbarfmt='%.2e', - stationsongrids=df_stats.stationsongrids, dtype='float32') + save_gridfile( + df_stats.grid_variogram_rmse, + 'grid_variogram_rmse', + gridfile_name, + df_stats.plotbbox, + df_stats.spacing, + df_stats.unit, + colorbarfmt='%.2e', + stationsongrids=df_stats.stationsongrids, + dtype='float32', + ) if isinstance(df_stats.grid_range, np.ndarray): # plot range heatmap - logger.info("- Plot variogram range per gridcell.") - df_stats(df_stats.grid_range, 'grid_range', workdir=os.path.join(workdir, 'figures'), - colorbarfmt='%1i', drawgridlines=drawgridlines, stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot variogram range per gridcell.') + df_stats( + df_stats.grid_range, + 'grid_range', + workdir=os.path.join(workdir, 'figures'), + colorbarfmt='%1i', + drawgridlines=drawgridlines, + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) if isinstance(df_stats.grid_variance, np.ndarray): # plot sill heatmap - logger.info("- Plot variogram sill per gridcell.") - df_stats(df_stats.grid_variance, 'grid_variance', workdir=os.path.join(workdir, 'figures'), drawgridlines=drawgridlines, - colorbarfmt='%.3e', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot variogram sill per gridcell.') + df_stats( + df_stats.grid_variance, + 'grid_variance', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.3e', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) if isinstance(df_stats.grid_variogram_rmse, np.ndarray): # plot variogram rmse heatmap - logger.info("- Plot variogram RMSE per gridcell.") - df_stats(df_stats.grid_variogram_rmse, 'grid_variogram_rmse', workdir=os.path.join(workdir, 'figures'), drawgridlines=drawgridlines, - colorbarfmt='%.2e', stationsongrids=stationsongrids, plotFormat=plot_fmt, userTitle=user_title) + logger.info('- Plot variogram RMSE per gridcell.') + df_stats( + df_stats.grid_variogram_rmse, + 'grid_variogram_rmse', + workdir=os.path.join(workdir, 'figures'), + drawgridlines=drawgridlines, + colorbarfmt='%.2e', + stationsongrids=stationsongrids, + plotFormat=plot_fmt, + userTitle=user_title, + ) + def main() -> None: inps = cmd_line_parse() @@ -2052,5 +3464,5 @@ def main() -> None: inps.variogramplot, inps.binnedvariogram, inps.variogram_per_timeslice, - inps.variogram_errlimit + inps.variogram_errlimit, ) diff --git a/tools/RAiDER/cli/validators.py b/tools/RAiDER/cli/validators.py index e64081978..30493a6f6 100755 --- a/tools/RAiDER/cli/validators.py +++ b/tools/RAiDER/cli/validators.py @@ -16,21 +16,22 @@ from RAiDER.utilFcns import rio_extents, rio_profile -_BUFFER_SIZE = 0.2 # default buffer size in lat/lon degrees +_BUFFER_SIZE = 0.2 # default buffer size in lat/lon degrees + def enforce_wm(value, aoi): - model = value.upper().replace("-", "") + model = value.upper().replace('-', '') try: _, model_obj = modelName2Module(model) except ModuleNotFoundError: raise NotImplementedError( - dedent(f''' + dedent(f""" Model {model} is not yet fully implemented, please contribute! - ''') + """) ) - ## check the user requsted bounding box is within the weather model domain + # check the user requsted bounding box is within the weather model domain modObj = model_obj().checkValidBounds(aoi.bounds()) return modObj @@ -40,22 +41,24 @@ def get_los(args): if args.get('orbit_file'): if args.get('ray_trace'): from RAiDER.losreader import Raytracing + los = Raytracing(args.orbit_file) else: los = Conventional(args.orbit_file) elif args.get('los_file'): if args.ray_trace: from RAiDER.losreader import Raytracing + los = Raytracing(args.los_file, args.los_convention) else: los = Conventional(args.los_file, args.los_convention) elif args.get('los_cube'): raise NotImplementedError('LOS_cube is not yet implemented') -# if args.ray_trace: -# los = Raytracing(args.los_cube) -# else: -# los = Conventional(args.los_cube) + # if args.ray_trace: + # los = Raytracing(args.los_cube) + # else: + # los = Conventional(args.los_cube) else: los = Zenith() @@ -67,13 +70,13 @@ def get_heights(args, out, station_file, bounding_box=None): dem_path = out out = { - 'dem': args.get('dem'), - 'height_file_rdr': None, - 'height_levels': None, - } + 'dem': args.get('dem'), + 'height_file_rdr': None, + 'height_levels': None, + } if args.get('dem'): - if (station_file is not None): + if station_file is not None: if 'Hgt_m' not in pd.read_csv(station_file): out['dem'] = os.path.join(dem_path, 'GLO30.dem') elif os.path.exists(args.dem): @@ -89,15 +92,14 @@ def get_heights(args, out, station_file, bounding_box=None): lats, lons, buf=_BUFFER_SIZE, - ) + ), ): raise ValueError( - 'Existing DEM does not cover the area of the input lat/lon ' - 'points; either move the DEM, delete it, or change the input ' - 'points.' - ) + 'Existing DEM does not cover the area of the input lat/lon points; either move the DEM, delete ' + 'it, or change the input points.' + ) else: - pass # will download the dem later + pass # will download the dem later elif args.get('height_file_rdr'): out['height_file_rdr'] = args.height_file_rdr @@ -114,9 +116,10 @@ def get_heights(args, out, station_file, bounding_box=None): out['height_levels'] = np.array([float(ll) for ll in l]) if np.any(out['height_levels'] < 0): - logger.warning('Weather model only extends to the surface topography; ' - 'height levels below the topography will be interpolated from the surface ' - 'and may be inaccurate.') + logger.warning( + 'Weather model only extends to the surface topography; ' + 'height levels below the topography will be interpolated from the surface and may be inaccurate.' + ) return out @@ -129,9 +132,9 @@ def get_query_region(args): query = GeocodedFile(args.dem, is_dem=True) elif args.get('lat_file'): - hgt_file = args.get('height_file_rdr') # only get it if exists + hgt_file = args.get('height_file_rdr') # only get it if exists dem_file = args.get('dem') - query = RasterRDR(args.lat_file, args.lon_file, hgt_file, dem_file) + query = RasterRDR(args.lat_file, args.lon_file, hgt_file, dem_file) elif args.get('station_file'): query = StationFile(args.station_file) @@ -143,16 +146,16 @@ def get_query_region(args): query = BoundingBox(bbox) elif args.get('geocoded_file'): - gfile = os.path.basename(args.geocoded_file).upper() - if (gfile.startswith('SRTM') or gfile.startswith('GLO')): + gfile = os.path.basename(args.geocoded_file).upper() + if gfile.startswith('SRTM') or gfile.startswith('GLO'): logger.debug('Using user DEM: %s', gfile) is_dem = True else: is_dem = False - query = GeocodedFile(args.geocoded_file, is_dem=is_dem) + query = GeocodedFile(args.geocoded_file, is_dem=is_dem) - ## untested + # untested elif args.get('geo_cube'): query = Geocube(args.geo_cube) @@ -172,7 +175,7 @@ def enforce_bbox(bbox): # Check the bbox if len(bbox) != 4: - raise ValueError("bounding box must have 4 elements!") + raise ValueError('bounding box must have 4 elements!') S, N, W, E = bbox if N <= S or E <= W: @@ -184,7 +187,9 @@ def enforce_bbox(bbox): for we in (W, E): if we < -180 or we > 180: - raise ValueError('Lons are out of W/E bounds (-180 to 180); Lons in the format of (0 to 360) are not supported.') + raise ValueError( + 'Lons are out of W/E bounds (-180 to 180); Lons in the format of (0 to 360) are not supported.' + ) return bbox @@ -210,7 +215,7 @@ def parse_dates(arg_dict): end = arg_dict['date_end'] end = enforce_valid_dates(end) else: - end = start + end = start if arg_dict.get('date_step'): step = int(arg_dict['date_step']) @@ -237,10 +242,7 @@ def enforce_valid_dates(arg): except ValueError: pass - - raise ValueError( - f'Unable to coerce {arg} to a date. Try %Y-%m-%d' - ) + raise ValueError(f'Unable to coerce {arg} to a date. Try %Y-%m-%d') def enforce_time(arg_dict): @@ -275,10 +277,7 @@ def convert_time(inp): 'Z', '%z', ) - all_formats = map( - ''.join, - itertools.product(time_formats, timezone_formats) - ) + all_formats = map(''.join, itertools.product(time_formats, timezone_formats)) for tf in all_formats: try: @@ -286,10 +285,7 @@ def convert_time(inp): except ValueError: pass - raise ValueError( - 'Unable to coerce {} to a time.'+ - 'Try T%H:%M:%S'.format() - ) + raise ValueError(f'Unable to coerce "{inp}" to a time. Try T%H:%M:%S') def modelName2Module(model_name): @@ -312,7 +308,7 @@ def modelName2Module(model_name): return module_name, wmObject -def getBufferedExtent(lats, lons=None, buf=0.): +def getBufferedExtent(lats, lons=None, buf=0.0): """Get the bounding box around a set of lats/lons.""" if lons is None: lats, lons = lats[..., 0], lons[..., 1] @@ -361,7 +357,7 @@ def isInside(extent1, extent2) -> bool: return np.all([t1, t2, t3, t4]) -## below are for downloadGNSSDelays +# below are for downloadGNSSDelays def date_type(arg): """Parse a date from a string in pseudo-ISO 8601 format.""" year_formats = ( @@ -377,9 +373,7 @@ def date_type(arg): except ValueError: pass - raise ArgumentTypeError( - f'Unable to coerce {arg} to a date. Try %Y-%m-%d' - ) + raise ArgumentTypeError(f'Unable to coerce {arg} to a date. Try %Y-%m-%d') class MappingType: @@ -394,6 +388,7 @@ class MappingType: assert mapping("hello") is None ``` """ + UNSET = object() def __init__(self, **kwargs) -> None: @@ -410,9 +405,7 @@ def __call__(self, arg): return self.mapping[arg] if self._default is self.UNSET: - raise KeyError( - f"Invalid choice '{arg}', must be one of {list(self.mapping.keys())}" - ) + raise KeyError(f"Invalid choice '{arg}', must be one of {list(self.mapping.keys())}") return self._default @@ -438,9 +431,9 @@ def __call__(self, arg): integer = int(arg) if self.lo is not None and integer < self.lo: - raise ArgumentTypeError(f"Must be greater than {self.lo}") + raise ArgumentTypeError(f'Must be greater than {self.lo}') if self.hi is not None and integer > self.hi: - raise ArgumentTypeError(f"Must be less than {self.hi}") + raise ArgumentTypeError(f'Must be less than {self.hi}') return integer @@ -484,10 +477,10 @@ def __init__( choices=None, required=False, help=None, - metavar=None + metavar=None, ) -> None: if type is not date_type: - raise ValueError("type must be `date_type`!") + raise ValueError('type must be `date_type`!') super().__init__( option_strings=option_strings, @@ -499,12 +492,12 @@ def __init__( choices=choices, required=required, help=help, - metavar=metavar + metavar=metavar, ) def __call__(self, parser, namespace, values, option_string=None): if len(values) > 3 or not values: - raise ArgumentError(self, "Only 1, 2 dates, or 2 dates and interval may be supplied") + raise ArgumentError(self, 'Only 1, 2 dates, or 2 dates and interval may be supplied') if len(values) == 2: start, end = values @@ -513,13 +506,12 @@ def __call__(self, parser, namespace, values, option_string=None): start, end, stepsize = values if not isinstance(stepsize.day, int): - raise ArgumentError(self, "The stepsize should be in integer days") + raise ArgumentError(self, 'The stepsize should be in integer days') new_year = date(year=stepsize.year, month=1, day=1) stepsize = (stepsize - new_year).days + 1 - values = [start + timedelta(days=k) - for k in range(0, (end - start).days + 1, stepsize)] + values = [start + timedelta(days=k) for k in range(0, (end - start).days + 1, stepsize)] setattr(namespace, self.dest, values) @@ -538,10 +530,10 @@ def __init__( choices=None, required=False, help=None, - metavar=None + metavar=None, ) -> None: if nargs != 4: - raise ValueError("nargs must be 4!") + raise ValueError('nargs must be 4!') super().__init__( option_strings=option_strings, @@ -553,7 +545,7 @@ def __init__( choices=choices, required=required, help=help, - metavar=metavar + metavar=metavar, ) def __call__(self, parser, namespace, values, option_string=None): @@ -568,6 +560,9 @@ def __call__(self, parser, namespace, values, option_string=None): for we in (W, E): if we < -180 or we > 180: - raise ArgumentError(self, 'Lons are out of W/E bounds (-180 to 180); Lons in the format of (0 to 360) are not supported.') + raise ArgumentError( + self, + 'Lons are out of W/E bounds (-180 to 180); Lons in the format of (0 to 360) are not supported.', + ) setattr(namespace, self.dest, values) diff --git a/tools/RAiDER/constants.py b/tools/RAiDER/constants.py index 67066e9e3..0bfa37bef 100644 --- a/tools/RAiDER/constants.py +++ b/tools/RAiDER/constants.py @@ -8,18 +8,16 @@ import numpy as np -_ZMIN = np.float64(-100) # minimum required height +_ZMIN = np.float64(-100) # minimum required height _ZREF = np.float64(26000) # maximum integration height when not specified by user -_STEP = np.float64(15.0) # integration step size in meters +_STEP = np.float64(15.0) # integration step size in meters -_g0 = np.float64(9.80665) # Standard gravitational constant -_g1 = np.float64(9.80616) # Gravitational constant @ 45° latitude used for corrections of earth's centrifugal force +_g0 = np.float64(9.80665) # Standard gravitational constant +_g1 = np.float64(9.80616) # Gravitational constant @ 45° latitude used for corrections of earth's centrifugal force _RE = np.float64(6371008.7714) R_EARTH_MAX_WGS84 = 6378137 R_EARTH_MIN_WGS84 = 6356752 _CUBE_SPACING_IN_M = float(2000) # Horizontal spacing of cube -_THRESHOLD_SECONDS = 1 * 60 # Threshold delta_time in seconds - - +_THRESHOLD_SECONDS = 1 * 60 # Threshold delta_time in seconds diff --git a/tools/RAiDER/delay.py b/tools/RAiDER/delay.py index f1eda9d90..2bbe69804 100755 --- a/tools/RAiDER/delay.py +++ b/tools/RAiDER/delay.py @@ -12,6 +12,7 @@ models are accessed as NETCDF files and should have "wet" "hydro" "wet_total" and "hydro_total" fields specified. """ + import os from datetime import datetime, timezone from typing import List, Union @@ -29,14 +30,14 @@ ############################################################################### def tropo_delay( - dt, - weather_model_file: str, - aoi, - los, - height_levels: List[float]=None, - out_proj: Union[int, str] =4326, - zref: Union[int, float]=_ZREF, - ): + dt, + weather_model_file: str, + aoi, + los, + height_levels: List[float] = None, + out_proj: Union[int, str] = 4326, + zref: Union[int, float] = _ZREF, +): """ Calculate integrated delays on query points. Options are: 1. Zenith delays (ZTD) @@ -63,13 +64,15 @@ def tropo_delay( try: wm_proj = CRS.from_wkt(ds['proj'].attrs['crs_wkt']) except KeyError: - logger.warning("WARNING: I can't find a CRS in the weather model file, so I will assume you are using WGS84") + logger.warning( + "WARNING: I can't find a CRS in the weather model file, so I will assume you are using WGS84" + ) wm_proj = CRS.from_epsg(4326) # get heights with xarray.load_dataset(weather_model_file) as ds: wm_levels = ds.z.values - toa = wm_levels.max() - 1 + toa = wm_levels.max() - 1 if height_levels is None: if aoi.type() == 'Geocube': @@ -82,13 +85,12 @@ def tropo_delay( if zref > toa: zref = toa - logger.warning('Requested integration height (zref) is higher than top of weather model. Forcing to top ({toa}).') + logger.warning( + 'Requested integration height (zref) is higher than top of weather model. Forcing to top ({toa}).' + ) - - #TODO: expose this as library function - ds = _get_delays_on_cube( - dt, weather_model_file, wm_proj, aoi, height_levels, los, crs, zref - ) + # TODO: expose this as library function + ds = _get_delays_on_cube(dt, weather_model_file, wm_proj, aoi, height_levels, los, crs, zref) if (aoi.type() == 'bounding_box') or (aoi.type() == 'Geocube'): return ds, None @@ -106,7 +108,7 @@ def tropo_delay( pnts = transformPoints(lats, lons, hgts, pnt_proj, out_proj) try: - ifWet, ifHydro = getInterpolators(ds, "ztd") + ifWet, ifHydro = getInterpolators(ds, 'ztd') except RuntimeError: logger.exception('Failed to get weather model %s interpolators.', weather_model_file) @@ -117,7 +119,7 @@ def tropo_delay( if los.is_Projected(): los.setTime(dt) los.setPoints(lats, lons, hgts) - wetDelay = los(wetDelay) + wetDelay = los(wetDelay) hydroDelay = los(hydroDelay) return wetDelay, hydroDelay @@ -138,28 +140,25 @@ def _get_delays_on_cube(dt, weather_model_file, wm_proj, aoi, heights, los, crs, # If no orbit is provided if los.is_Zenith() or los.is_Projected(): - out_type = ["zenith" if los.is_Zenith() else 'slant - projected'][0] + out_type = ['zenith' if los.is_Zenith() else 'slant - projected'][0] # Get ZTD interpolators try: - ifWet, ifHydro = getInterpolators(weather_model_file, "total") + ifWet, ifHydro = getInterpolators(weather_model_file, 'total') except RuntimeError: logger.exception('Failed to get weather model %s interpolators.', weather_model_file) - # Build cube - wetDelay, hydroDelay = _build_cube( - aoi.xpts, aoi.ypts, zpts, - wm_proj, crs, [ifWet, ifHydro]) + wetDelay, hydroDelay = _build_cube(aoi.xpts, aoi.ypts, zpts, wm_proj, crs, [ifWet, ifHydro]) else: - out_type = "slant - raytracing" + out_type = 'slant - raytracing' # Get pointwise interpolators try: ifWet, ifHydro = getInterpolators( weather_model_file, - kind="pointwise", + kind='pointwise', shared=(nproc > 1), ) except RuntimeError: @@ -168,9 +167,8 @@ def _get_delays_on_cube(dt, weather_model_file, wm_proj, aoi, heights, los, crs, # Build cube if nproc == 1: wetDelay, hydroDelay = _build_cube_ray( - aoi.xpts, aoi.ypts, zpts, los, - wm_proj, crs, - [ifWet, ifHydro], MAX_TROPO_HEIGHT=zref) + aoi.xpts, aoi.ypts, zpts, los, wm_proj, crs, [ifWet, ifHydro], MAX_TROPO_HEIGHT=zref + ) ### Use multi-processing here else: @@ -185,8 +183,7 @@ def _get_delays_on_cube(dt, weather_model_file, wm_proj, aoi, heights, los, crs, logger.critical('There are missing delay values. Check your inputs.') # Write output file - ds = writeResultsToXarray(dt, aoi.xpts, aoi.ypts, zpts, crs, wetDelay, - hydroDelay, weather_model_file, out_type) + ds = writeResultsToXarray(dt, aoi.xpts, aoi.ypts, zpts, crs, wetDelay, hydroDelay, weather_model_file, out_type) return ds @@ -197,32 +194,35 @@ def _build_cube(xpts, ypts, zpts, model_crs, pts_crs, interpolators): xx, yy = np.meshgrid(xpts, ypts) # Output arrays - outputArrs = [np.zeros((zpts.size, ypts.size, xpts.size)) - for mm in range(len(interpolators))] - + outputArrs = [np.zeros((zpts.size, ypts.size, xpts.size)) for mm in range(len(interpolators))] # Loop over heights and compute delays for ii, ht in enumerate(zpts): - # pts is in weather model system; if model_crs != pts_crs: # lat / lon / height for hrrr - pts = transformPoints(yy, xx, np.full(yy.shape, ht), - pts_crs, model_crs) + pts = transformPoints(yy, xx, np.full(yy.shape, ht), pts_crs, model_crs) else: pts = np.stack([yy, xx, np.full(yy.shape, ht)], axis=-1) for mm, intp in enumerate(interpolators): - outputArrs[mm][ii,...] = intp(pts) + outputArrs[mm][ii, ...] = intp(pts) return outputArrs def _build_cube_ray( - xpts, ypts, zpts, los, model_crs, - pts_crs, interpolators, outputArrs=None, MAX_SEGMENT_LENGTH=1000., - MAX_TROPO_HEIGHT=_ZREF, - ): + xpts, + ypts, + zpts, + los, + model_crs, + pts_crs, + interpolators, + outputArrs=None, + MAX_SEGMENT_LENGTH=1000.0, + MAX_TROPO_HEIGHT=_ZREF, +): """ Iterate over interpolators and build a cube using raytracing. @@ -240,8 +240,7 @@ def _build_cube_ray( output_created_here = False if outputArrs is None: output_created_here = True - outputArrs = [np.zeros((zpts.size, ypts.size, xpts.size)) - for mm in range(len(interpolators))] + outputArrs = [np.zeros((zpts.size, ypts.size, xpts.size)) for mm in range(len(interpolators))] # Various transformers needed here epsg4326 = CRS.from_epsg(4326) @@ -250,7 +249,7 @@ def _build_cube_ray( # Loop over heights of output cube and compute delays for hh, ht in enumerate(zpts): - logger.info(f"Processing slice {hh+1} / {len(zpts)}: {ht}") + logger.info(f'Processing slice {hh+1} / {len(zpts)}: {ht}') # Slices to fill on output outSubs = [x[hh, ...] for x in outputArrs] @@ -266,22 +265,21 @@ def _build_cube_ray( LOS = los.getLookVectors(ht, llh, xyz, yy) # Step 3 - Determine delays between each model height per ray - ray_lengths, low_xyzs, high_xyzs = \ - build_ray(model_zs, ht, xyz, LOS, MAX_TROPO_HEIGHT) + ray_lengths, low_xyzs, high_xyzs = build_ray(model_zs, ht, xyz, LOS, MAX_TROPO_HEIGHT) # if the top most height layer doesnt contribute to the integral, skip it if ray_lengths is None and ht == zpts[-1]: continue elif np.isnan(ray_lengths).all(): - raise ValueError("geo2rdr did not converge. Check orbit coverage") + raise ValueError('geo2rdr did not converge. Check orbit coverage') # Determine number of parts to break ray into (this is what gets integrated over) - nParts = np.ceil(ray_lengths.max((1,2)) / MAX_SEGMENT_LENGTH).astype(int) + 1 + nParts = np.ceil(ray_lengths.max((1, 2)) / MAX_SEGMENT_LENGTH).astype(int) + 1 # iterate over weather model height levels for zz, nparts in enumerate(nParts): - fracs = np.linspace(0., 1., num=nparts) + fracs = np.linspace(0.0, 1.0, num=nparts) # Integrate over chunks of ray for findex, ff in enumerate(fracs): @@ -289,11 +287,7 @@ def _build_cube_ray( pts_xyz = low_xyzs[zz] + ff * (high_xyzs[zz] - low_xyzs[zz]) # Ray point in model coordinates (x, y, z) - pts = ecef_to_model.transform( - pts_xyz[..., 0], - pts_xyz[..., 1], - pts_xyz[..., 2] - ) + pts = ecef_to_model.transform(pts_xyz[..., 0], pts_xyz[..., 1], pts_xyz[..., 2]) # Order for the interpolator (from xyz to yxz) pts = np.stack((pts[1], pts[0], pts[2]), axis=-1) @@ -312,12 +306,12 @@ def _build_cube_ray( pts[:, :, -1] = np.array(model_zs).max() # Trapezoidal integration with scaling - wt = 0.5 if findex in [0, fracs.size-1] else 1.0 - wt *= ray_lengths[zz] *1.0e-6 / (nparts - 1.0) + wt = 0.5 if findex in [0, fracs.size - 1] else 1.0 + wt *= ray_lengths[zz] * 1.0e-6 / (nparts - 1.0) # For each interpolator, integrate between levels for mm, out in enumerate(outSubs): - val = interpolators[mm](pts) + val = interpolators[mm](pts) # TODO - This should not occur if there is enough padding in model # val[np.isnan(val)] = 0.0 @@ -329,75 +323,79 @@ def _build_cube_ray( def writeResultsToXarray(dt, xpts, ypts, zpts, crs, wetDelay, hydroDelay, weather_model_file, out_type): """Write a 1-D array to a NETCDF5 file.""" - # Modify this as needed for NISAR / other projects + # Modify this as needed for NISAR / other projects ds = xarray.Dataset( data_vars=dict( - wet=(["z", "y", "x"], - wetDelay, - {"units" : "m", - "description": f"wet {out_type} delay", - # 'crs': crs.to_epsg(), - "grid_mapping": "crs", - - }), - hydro=(["z", "y", "x"], - hydroDelay, - {"units": "m", + wet=( + ['z', 'y', 'x'], + wetDelay, + { + 'units': 'm', + 'description': f'wet {out_type} delay', # 'crs': crs.to_epsg(), - "description": f"hydrostatic {out_type} delay", - "grid_mapping": "crs", - }), + 'grid_mapping': 'crs', + }, + ), + hydro=( + ['z', 'y', 'x'], + hydroDelay, + { + 'units': 'm', + # 'crs': crs.to_epsg(), + 'description': f'hydrostatic {out_type} delay', + 'grid_mapping': 'crs', + }, + ), ), coords=dict( - x=(["x"], xpts), - y=(["y"], ypts), - z=(["z"], zpts), + x=(['x'], xpts), + y=(['y'], ypts), + z=(['z'], zpts), ), attrs=dict( - Conventions="CF-1.7", - title="RAiDER geo cube", + Conventions='CF-1.7', + title='RAiDER geo cube', source=os.path.basename(weather_model_file), - history=str(datetime.now(tz=timezone.utc)) + " RAiDER", - description=f"RAiDER geo cube - {out_type}", - reference_time=dt.strftime("%Y%m%dT%H:%M:%S"), + history=str(datetime.now(tz=timezone.utc)) + ' RAiDER', + description=f'RAiDER geo cube - {out_type}', + reference_time=dt.strftime('%Y%m%dT%H:%M:%S'), ), ) # Write projection system mapping - ds["crs"] = -2147483647 # dummy placeholder + ds['crs'] = -2147483647 # dummy placeholder for k, v in crs.to_cf().items(): ds.crs.attrs[k] = v # Write z-axis information - ds.z.attrs["axis"] = "Z" - ds.z.attrs["units"] = "m" - ds.z.attrs["description"] = "height above ellipsoid" + ds.z.attrs['axis'] = 'Z' + ds.z.attrs['units'] = 'm' + ds.z.attrs['description'] = 'height above ellipsoid' # If in degrees - if crs.axis_info[0].unit_name == "degree": - ds.y.attrs["units"] = "degrees_north" - ds.y.attrs["standard_name"] = "latitude" - ds.y.attrs["long_name"] = "latitude" + if crs.axis_info[0].unit_name == 'degree': + ds.y.attrs['units'] = 'degrees_north' + ds.y.attrs['standard_name'] = 'latitude' + ds.y.attrs['long_name'] = 'latitude' - ds.x.attrs["units"] = "degrees_east" - ds.x.attrs["standard_name"] = "longitude" - ds.x.attrs["long_name"] = "longitude" + ds.x.attrs['units'] = 'degrees_east' + ds.x.attrs['standard_name'] = 'longitude' + ds.x.attrs['long_name'] = 'longitude' else: - ds.y.attrs["axis"] = "Y" - ds.y.attrs["standard_name"] = "projection_y_coordinate" - ds.y.attrs["long_name"] = "y-coordinate in projected coordinate system" - ds.y.attrs["units"] = "m" + ds.y.attrs['axis'] = 'Y' + ds.y.attrs['standard_name'] = 'projection_y_coordinate' + ds.y.attrs['long_name'] = 'y-coordinate in projected coordinate system' + ds.y.attrs['units'] = 'm' - ds.x.attrs["axis"] = "X" - ds.x.attrs["standard_name"] = "projection_x_coordinate" - ds.x.attrs["long_name"] = "x-coordinate in projected coordinate system" - ds.x.attrs["units"] = "m" + ds.x.attrs['axis'] = 'X' + ds.x.attrs['standard_name'] = 'projection_x_coordinate' + ds.x.attrs['long_name'] = 'x-coordinate in projected coordinate system' + ds.x.attrs['units'] = 'm' return ds - def transformPoints(lats: np.ndarray, lons: np.ndarray, hgts: np.ndarray, old_proj: CRS, new_proj: CRS) -> np.ndarray: """ Transform lat/lon/hgt data to an array of points in a new projection. @@ -423,7 +421,7 @@ def transformPoints(lats: np.ndarray, lons: np.ndarray, hgts: np.ndarray, old_pr # in_flip = old_proj.axis_info[0].direction # out_flip = new_proj.axis_info[0].direction - res = t.transform(lons, lats, hgts) + res = t.transform(lons, lats, hgts) # lat/lon/height - return np.stack([res[1], res[0], res[2]], axis=-1) + return np.stack([res[1], res[0], res[2]], axis=-1) diff --git a/tools/RAiDER/delayFcns.py b/tools/RAiDER/delayFcns.py index 8b97806e5..e6a2223d9 100755 --- a/tools/RAiDER/delayFcns.py +++ b/tools/RAiDER/delayFcns.py @@ -34,8 +34,8 @@ def getInterpolators(wm_file, kind='pointwise', shared=False): ys_wm = np.array(ds.variables['y'][:]) zs_wm = np.array(ds.variables['z'][:]) - wet = ds.variables['wet_total' if kind=='total' else 'wet'][:] - hydro = ds.variables['hydro_total' if kind=='total' else 'hydro'][:] + wet = ds.variables['wet_total' if kind == 'total' else 'wet'][:] + hydro = ds.variables['hydro_total' if kind == 'total' else 'hydro'][:] wet = np.array(wet).transpose(1, 2, 0) hydro = np.array(hydro).transpose(1, 2, 0) @@ -49,12 +49,11 @@ def getInterpolators(wm_file, kind='pointwise', shared=False): xs_wm = make_shared_raw(xs_wm) ys_wm = make_shared_raw(ys_wm) zs_wm = make_shared_raw(zs_wm) - wet = make_shared_raw(wet) + wet = make_shared_raw(wet) hydro = make_shared_raw(hydro) - - ifWet = Interpolator((ys_wm, xs_wm, zs_wm), wet, fill_value=np.nan, bounds_error = False) - ifHydro = Interpolator((ys_wm, xs_wm, zs_wm), hydro, fill_value=np.nan, bounds_error = False) + ifWet = Interpolator((ys_wm, xs_wm, zs_wm), wet, fill_value=np.nan, bounds_error=False) + ifHydro = Interpolator((ys_wm, xs_wm, zs_wm), hydro, fill_value=np.nan, bounds_error=False) return ifWet, ifHydro @@ -64,14 +63,11 @@ def make_shared_raw(inarr): # Create flat shared array if mp is None: raise ImportError('multiprocessing is not available') - + shared_arr = mp.RawArray('d', inarr.size) # Create a numpy view of it - shared_arr_np = np.ndarray(inarr.shape, dtype=np.float64, - buffer=shared_arr) + shared_arr_np = np.ndarray(inarr.shape, dtype=np.float64, buffer=shared_arr) # Copy data to shared array np.copyto(shared_arr_np, inarr) return shared_arr_np - - diff --git a/tools/RAiDER/dem.py b/tools/RAiDER/dem.py index ecceacae7..c6d2f116a 100644 --- a/tools/RAiDER/dem.py +++ b/tools/RAiDER/dem.py @@ -16,19 +16,19 @@ def download_dem( - ll_bounds=None, - demName='warpedDEM.dem', - overwrite=False, - writeDEM=False, - buf=0.02, - ): - """ - Download a DEM if one is not already present. + ll_bounds=None, + demName='warpedDEM.dem', + overwrite=False, + writeDEM=False, + buf=0.02, +): + """ + Download a DEM if one is not already present. Args: llbounds: list/ndarry of floats -lat/lon bounds of the area to download. Values should be ordered in the following way: [S, N, W, E] writeDEM: boolean -write the DEM to file - outName: string -name of the DEM file + outName: string -name of the DEM file buf: float -buffer to add to the bounds overwrite: boolean -overwrite existing DEM Returns: @@ -48,14 +48,16 @@ def download_dem( if not download: logger.info('Using existing DEM: %s', demName) - zvals, metadata = rio_open(demName, returnProj=True) + zvals, metadata = rio_open(demName, returnProj=True) else: # download the dem # inExtent is SNWE # dem-stitcher wants WSEN bounds = [ - np.floor(ll_bounds[2]) - buf, np.floor(ll_bounds[0]) - buf, - np.ceil(ll_bounds[3]) + buf, np.ceil(ll_bounds[1]) + buf + np.floor(ll_bounds[2]) - buf, + np.floor(ll_bounds[0]) - buf, + np.ceil(ll_bounds[3]) + buf, + np.ceil(ll_bounds[1]) + buf, ] zvals, metadata = stitch_dem( diff --git a/tools/RAiDER/getStationDelays.py b/tools/RAiDER/getStationDelays.py index 88fffcb96..3e78987e2 100644 --- a/tools/RAiDER/getStationDelays.py +++ b/tools/RAiDER/getStationDelays.py @@ -24,16 +24,16 @@ def get_delays_UNR(stationFile, filename, dateList, returnTime=None) -> None: Parses and returns a dictionary containing either (1) all the GPS delays, if returnTime is None, or (2) only the delay at the closest times to to returnTime. - + Args: stationFile: binary - a .gz station delay file - filename: ? - ? + filename: ? - ? dateList: list of datetime - ? returnTime: datetime - specified time of GPS delay (default all times) Returns: None - + The function writes a CSV file containing the times and delay information (delay in mm, delay uncertainty, delay gradients) @@ -43,19 +43,19 @@ def get_delays_UNR(stationFile, filename, dateList, returnTime=None) -> None: Wet and hydrostratic delays were derived as so: Constants —> k1 = 0.704, k2 = 0.776, k3 = 3739.0, m = 18.0152/28.9644, k2' = k2-(k1*m) = 0.33812796398337275, Rv = 461.5 J/(kg·K), ρl = 997 kg/m^3 - - *NOTE: wet delays passed here are computed using - PMV = precipitable water vapor, - P = total atm pressure, + + *NOTE: wet delays passed here are computed using + PMV = precipitable water vapor, + P = total atm pressure, Tm = mean temp of the column, as: Wet zenith delay = 10^-6 ρlRv(k2' + k3/Tm) PMV Hydrostatic zenith delay = Total zenith delay - wet zenith delay = k1*(P/Tm) - + Source —> Hanssen, R. F. (2001) eqns. 6.2.7-10 - *NOTE: Due to a formatting error in the tropo SINEX files, the two - tropospheric gradient columns (TGNTOT and TGETOT) are interchanged, + *NOTE: Due to a formatting error in the tropo SINEX files, the two + tropospheric gradient columns (TGNTOT and TGETOT) are interchanged, as are the formal error columns (_SIG). Source —> http://geodesy.unr.edu/gps_timeseries/README_trop2.txt) @@ -97,19 +97,21 @@ def get_delays_UNR(stationFile, filename, dateList, returnTime=None) -> None: try: split_lines = line.split() # units: mm, mm, mm, deg, deg, deg, deg, mm, mm, K - trotot, trototSD, trwet, tgetot, tgetotSD, tgntot, tgntotSD, wvapor, wvaporSD, mtemp = \ - (float(t) for t in split_lines[2:]) + trotot, trototSD, trwet, tgetot, tgetotSD, tgntot, tgntotSD, wvapor, wvaporSD, mtemp = ( + float(t) for t in split_lines[2:] + ) except BaseException: # TODO: What error(s)? continue site = split_lines[0] - year, doy, seconds = (int(n) - for n in split_lines[1].split(':')) + year, doy, seconds = (int(n) for n in split_lines[1].split(':')) # Break iteration if time from line in file does not match date reported in filename if doy != doyFromFile: logger.warning( 'time %s from line in conflict with time %s from file ' '%s, will continue reading next tarfile(s)', - doy, doyFromFile, j + doy, + doyFromFile, + j, ) continue # convert units from mm to m @@ -124,16 +126,15 @@ def get_delays_UNR(stationFile, filename, dateList, returnTime=None) -> None: # Break iteration if file contains no data. if d == []: logger.warning( - 'file %s for station %s is empty, will continue reading next ' - 'tarfile(s)', j, j.split('.')[0] + 'file %s for station %s is empty, will continue reading next tarfile(s)', + j, j.split('.')[0] ) continue # check for missing times true_times = list(range(0, 86400, 300)) if len(timesList) != len(true_times): - missing = [ - True if t not in timesList else False for t in true_times] + missing = [t not in timesList for t in true_times] mask = np.array(missing) delay, sig, wet_delay, hydro_delay = [np.full((288,), np.nan)] * 4 delay[~mask] = d @@ -150,14 +151,29 @@ def get_delays_UNR(stationFile, filename, dateList, returnTime=None) -> None: # if time not specified, pass all times if returnTime is None: - filtoutput = {'ID': [site] * len(wet_delay), 'Date': [time] * len(wet_delay), 'ZTD': delay, 'wet_delay': wet_delay, - 'hydrostatic_delay': hydro_delay, 'times': times, 'sigZTD': sig} - filtoutput = [{key: value[k] for key, value in filtoutput.items()} - for k in range(len(filtoutput['ID']))] + filtoutput = { + 'ID': [site] * len(wet_delay), + 'Date': [time] * len(wet_delay), + 'ZTD': delay, + 'wet_delay': wet_delay, + 'hydrostatic_delay': hydro_delay, + 'times': times, + 'sigZTD': sig, + } + filtoutput = [{key: value[k] for key, value in filtoutput.items()} for k in range(len(filtoutput['ID']))] else: index = np.argmin(np.abs(np.array(timesList) - returnTime)) - filtoutput = [{'ID': site, 'Date': time, 'ZTD': delay[index], 'wet_delay': wet_delay[index], - 'hydrostatic_delay': hydro_delay[index], 'times': times[index], 'sigZTD': sig[index]}] + filtoutput = [ + { + 'ID': site, + 'Date': time, + 'ZTD': delay[index], + 'wet_delay': wet_delay[index], + 'hydrostatic_delay': hydro_delay[index], + 'times': times[index], + 'sigZTD': sig[index], + } + ] # setup pandas array and write output to CSV, making sure to update existing CSV. filtoutput = pd.DataFrame(filtoutput) if os.path.exists(filename): @@ -166,13 +182,10 @@ def get_delays_UNR(stationFile, filename, dateList, returnTime=None) -> None: filtoutput.to_csv(filename, index=False) # record all used tar files - allstationTarfiles.extend([os.path.join(stationFile, k) - for k in stationTarlist]) + allstationTarfiles.extend([os.path.join(stationFile, k) for k in stationTarlist]) allstationTarfiles.sort() del ziprepo - return - def get_station_data(inFile, dateList, gps_repo=None, numCPUs=8, outDir=None, returnTime=None) -> None: """Pull tropospheric delay data for a given station name.""" @@ -186,13 +199,12 @@ def get_station_data(inFile, dateList, gps_repo=None, numCPUs=8, outDir=None, re returnTime = seconds_of_day(returnTime) # print warning if not divisible by 3 seconds if returnTime % 3 != 0: - index = np.argmin( - np.abs(np.array(list(range(0, 86400, 300))) - returnTime)) - updatedreturnTime = str(dt.timedelta( - seconds=list(range(0, 86400, 300))[index])) + index = np.argmin(np.abs(np.array(list(range(0, 86400, 300))) - returnTime)) + updatedreturnTime = str(dt.timedelta(seconds=list(range(0, 86400, 300))[index])) logger.warning( - 'input time %s not divisible by 3 seconds, so next closest time %s ' - 'will be chosen', returnTime, updatedreturnTime + 'input time %s not divisible by 3 seconds, so next closest time %s will be chosen', + returnTime, + updatedreturnTime, ) returnTime = updatedreturnTime @@ -225,13 +237,15 @@ def get_station_data(inFile, dateList, gps_repo=None, numCPUs=8, outDir=None, re # drop all duplicate lines statsFile.drop_duplicates(inplace=True) # Convert the above object into a csv file and export - statsFile.to_csv(name, index=False, encoding="utf-8") + statsFile.to_csv(name, index=False, encoding='utf-8') del statsFile # Add lat/lon/height info origstatsFile = pd.read_csv(inFile) statsFile = pd.read_csv(name) - statsFile = pd.merge(left=statsFile, right=origstatsFile[['ID', 'Lat', 'Lon', 'Hgt_m']], how='left', left_on='ID', right_on='ID') + statsFile = pd.merge( + left=statsFile, right=origstatsFile[['ID', 'Lat', 'Lon', 'Hgt_m']], how='left', left_on='ID', right_on='ID' + ) # drop all lines with nans and sort by station ID and year statsFile.dropna(how='any', inplace=True) # drop all duplicate lines @@ -255,5 +269,5 @@ def seconds_of_day(returnTime): if isinstance(returnTime, dt.time): h, m, s = returnTime.hour, returnTime.minute, returnTime.second else: - h, m, s = map(int, returnTime.split(":")) - return h * 3600 + m * 60 + s + h, m, s = map(int, returnTime.split(':')) + return h * 3600 + m * 60 + s diff --git a/tools/RAiDER/gnss/downloadGNSSDelays.py b/tools/RAiDER/gnss/downloadGNSSDelays.py index 193788151..db634ccbb 100755 --- a/tools/RAiDER/gnss/downloadGNSSDelays.py +++ b/tools/RAiDER/gnss/downloadGNSSDelays.py @@ -18,21 +18,21 @@ # base URL for UNR repository -_UNR_URL = "http://geodesy.unr.edu/" +_UNR_URL = 'http://geodesy.unr.edu/' def get_station_list( - bbox=None, - stationFile=None, - writeLoc=None, - name_appendix='', - writeStationFile=True, - ): + bbox=None, + stationFile=None, + writeLoc=None, + name_appendix='', + writeStationFile=True, +): """ Creates a list of stations inside a lat/lon bounding box from a source. Args: - bbox: list of float - length-4 list of floats that describes a bounding box. + bbox: list of float - length-4 list of floats that describes a bounding box. Format is S N W E station_file: str - Name of a .csv or .txt file to read containing station IDs writeStationFile: bool - Whether to write out the station dataframe to a .csv file @@ -52,7 +52,7 @@ def get_station_list( stations = [] with open(stationFile) as f: for k, line in enumerate(f): - if k ==0: + if k == 0: names = line.strip().split() else: stations.append([line.strip().split()]) @@ -60,10 +60,7 @@ def get_station_list( # write to file and pass final stations list if writeStationFile: - output_file = os.path.join( - writeLoc or os.getcwd(), - 'gnssStationList_overbbox' + name_appendix + '.csv' - ) + output_file = os.path.join(writeLoc or os.getcwd(), 'gnssStationList_overbbox' + name_appendix + '.csv') station_data.to_csv(output_file, index=False) return list(station_data['ID'].values), [output_file if writeStationFile else station_data][0] @@ -78,17 +75,12 @@ def get_stats_by_llh(llhBox=None, baseURL=_UNR_URL): llhBox = [-90, 90, 0, 360] S, N, W, E = llhBox if (W < 0) or (E < 0): - raise ValueError( - 'get_stats_by_llh: bounding box must be on lon range [0, 360]') + raise ValueError('get_stats_by_llh: bounding box must be on lon range [0, 360]') stationHoldings = f'{baseURL}NGLStationPages/llh.out' # it's a file like object and works just like a file - stations = pd.read_csv( - stationHoldings, - sep=r'\s+', - names=['ID', 'Lat', 'Lon', 'Hgt_m'] - ) + stations = pd.read_csv(stationHoldings, sep=r'\s+', names=['ID', 'Lat', 'Lon', 'Hgt_m']) # convert lons from [0, 360] to [-180, 180] stations['Lon'] = ((stations['Lon'].values + 180) % 360) - 180 @@ -99,15 +91,16 @@ def get_stats_by_llh(llhBox=None, baseURL=_UNR_URL): def download_tropo_delays( - stats, years, + stats, + years, gps_repo='UNR', writeDir='.', numCPUs=8, download=False, ) -> None: """ - Check for and download GNSS tropospheric delays from an archive. If - download is True then files will be physically downloaded, but this + Check for and download GNSS tropospheric delays from an archive. If + download is True then files will be physically downloaded, but this is not necessary as data can be virtually accessed. Args: @@ -136,21 +129,15 @@ def download_tropo_delays( with multiprocessing.Pool(numCPUs) as multipool: # only record valid path if gps_repo == 'UNR': - results = [ - fileurl for fileurl in multipool.starmap(download_UNR, stat_year_tup) - if fileurl['path'] - ] + results = [fileurl for fileurl in multipool.starmap(download_UNR, stat_year_tup) if fileurl['path']] else: - raise NotImplementedError( - f'download_tropo_delays: gps_repo "{gps_repo}" not yet implemented') + raise NotImplementedError(f'download_tropo_delays: gps_repo "{gps_repo}" not yet implemented') # Write results to file if len(results) == 0: - raise NoStationDataFoundError( - station_list=stats['ID'].to_list(), years=years) + raise NoStationDataFoundError(station_list=stats['ID'].to_list(), years=years) statDF = pd.DataFrame(results).set_index('ID') - statDF.to_csv(os.path.join( - writeDir, f'{gps_repo}gnssStationList_overbbox_withpaths.csv')) + statDF.to_csv(os.path.join(writeDir, f'{gps_repo}gnssStationList_overbbox_withpaths.csv')) def download_UNR(statID, year, writeDir='.', download=False, baseURL=_UNR_URL): @@ -163,9 +150,8 @@ def download_UNR(statID, year, writeDir='.', download=False, baseURL=_UNR_URL): """ if baseURL not in [_UNR_URL]: raise NotImplementedError(f'Data repository {baseURL} has not yet been implemented') - - URL = "{0}gps_timeseries/trop/{1}/{1}.{2}.trop.zip".format( - baseURL, statID.upper(), year) + + URL = '{0}gps_timeseries/trop/{1}/{1}.{2}.trop.zip'.format(baseURL, statID.upper(), year) logger.debug('Currently checking station %s in %s', statID, year) if download: saveLoc = os.path.abspath(os.path.join(writeDir, f'{statID.upper()}.{year}.trop.zip')) @@ -265,9 +251,7 @@ def main(inps=None) -> None: # iterate over years years = list(set([i.year for i in dateList])) - download_tropo_delays( - stats, years, gps_repo=gps_repo, writeDir=out, download=download - ) + download_tropo_delays(stats, years, gps_repo=gps_repo, writeDir=out, download=download) # Combine station data with URL info pathsdf = pd.read_csv(os.path.join(out, f'{gps_repo}gnssStationList_overbbox_withpaths.csv')) @@ -278,13 +262,12 @@ def main(inps=None) -> None: # Extract delays for each station dateList = [k.strftime('%Y-%m-%d') for k in dateList] get_station_data( - os.path.join( - out, f'{gps_repo}gnssStationList_overbbox_withpaths.csv'), + os.path.join(out, f'{gps_repo}gnssStationList_overbbox_withpaths.csv'), dateList, gps_repo=gps_repo, numCPUs=cpus, outDir=out, - returnTime=returnTime + returnTime=returnTime, ) logger.debug('Completed processing') @@ -296,14 +279,12 @@ def parse_bbox(bounding_box): try: bbox = [float(val) for val in bounding_box.split()] except ValueError: - raise Exception( - 'Cannot understand the --bbox argument. String input is incorrect or path does not exist.') + raise Exception('Cannot understand the --bbox argument. String input is incorrect or path does not exist.') elif isinstance(bounding_box, list): bbox = bounding_box else: - raise Exception( - 'Passing a file with a bounding box not yet supported.') + raise Exception('Passing a file with a bounding box not yet supported.') long_cross_zero = 1 if bbox[2] * bbox[3] < 0 else 0 @@ -336,10 +317,8 @@ def get_stats(bbox, long_cross_zero, out, station_file): else: if bbox[3] < bbox[2]: bbox[3] = 360.0 - stats, statdata = get_station_list( - bbox=bbox, stationFile=station_file, writeStationFile=False - ) - + stats, statdata = get_station_list(bbox=bbox, stationFile=station_file, writeStationFile=False) + statdata.to_csv(station_file, index=False) return stats, statdata @@ -370,8 +349,7 @@ def filterToBBox(stations, llhBox): index = k break if index is None: - raise KeyError( - 'filterToBBox: No valid column names found for latitude and longitude') + raise KeyError('filterToBBox: No valid column names found for latitude and longitude') lon_key = lon_keys[k] lat_key = lat_keys[k] @@ -379,6 +357,5 @@ def filterToBBox(stations, llhBox): # convert lon format to -180 to 180 W, E = (((D + 180) % 360) - 180 for D in [W, E]) - mask = (stations[lat_key] > S) & (stations[lat_key] < N) & ( - stations[lon_key] < E) & (stations[lon_key] > W) + mask = (stations[lat_key] > S) & (stations[lat_key] < N) & (stations[lon_key] < E) & (stations[lon_key] > W) return stations[mask] diff --git a/tools/RAiDER/gnss/processDelayFiles.py b/tools/RAiDER/gnss/processDelayFiles.py index b6fe3d72f..2cda4c9a8 100644 --- a/tools/RAiDER/gnss/processDelayFiles.py +++ b/tools/RAiDER/gnss/processDelayFiles.py @@ -24,6 +24,7 @@ def combineDelayFiles(outName, loc=os.getcwd(), source='model', ext='.csv', ref= if len(files) == 1: if source == 'model': import shutil + shutil.copy(files[0], outName) else: files = readZTDFile(files[0], col_name=col_name) @@ -36,21 +37,9 @@ def combineDelayFiles(outName, loc=os.getcwd(), source='model', ext='.csv', ref= print(f'Combining {source} delay files') try: - concatDelayFiles( - files, - sort_list=['ID', 'Datetime'], - outName=outName, - source=source - ) + concatDelayFiles(files, sort_list=['ID', 'Datetime'], outName=outName, source=source) except BaseException: - concatDelayFiles( - files, - sort_list=['ID', 'Date'], - outName=outName, - source=source, - ref=ref, - col_name=col_name - ) + concatDelayFiles(files, sort_list=['ID', 'Date'], outName=outName, source=source, ref=ref, col_name=col_name) def addDateTimeToFiles(fileList, force=False, verbose=False) -> None: @@ -77,10 +66,7 @@ def addDateTimeToFiles(fileList, force=False, verbose=False) -> None: data.drop_duplicates(inplace=True) data.to_csv(f, index=False) except (AttributeError, ValueError): - print( - f'File {f} does not contain datetime info, skipping' - - ) + print(f'File {f} does not contain datetime info, skipping') del data @@ -89,23 +75,18 @@ def getDateTime(filename): filename = os.path.basename(filename) dtr = re.compile(r'\d{8}T\d{6}') dt = dtr.search(filename) - return datetime.datetime.strptime( - dt.group(), - '%Y%m%dT%H%M%S' - ) + return datetime.datetime.strptime(dt.group(), '%Y%m%dT%H%M%S') def update_time(row, localTime_hrs): """Update with local origin time.""" - localTime_estimate = row['Datetime'].replace(hour=localTime_hrs, - minute=0, second=0) + localTime_estimate = row['Datetime'].replace(hour=localTime_hrs, minute=0, second=0) # determine if you need to shift days time_shift = datetime.timedelta(days=0) # round to nearest hour - days_diff = (row['Datetime'] - - datetime.timedelta(seconds=math.floor( - row['Localtime']) * 3600)).day - \ - localTime_estimate.day + days_diff = ( + row['Datetime'] - datetime.timedelta(seconds=math.floor(row['Localtime']) * 3600) + ).day - localTime_estimate.day # if lon <0, check if you need to add day if row['Lon'] < 0: # add day @@ -116,34 +97,30 @@ def update_time(row, localTime_hrs): # subtract day if days_diff != 0: time_shift = -datetime.timedelta(days=1) - return localTime_estimate + datetime.timedelta(seconds=row['Localtime'] - * 3600) + time_shift + return localTime_estimate + datetime.timedelta(seconds=row['Localtime'] * 3600) + time_shift def pass_common_obs(reference, target, localtime=None): """Pass only observations in target spatiotemporally common to reference.""" if isinstance(target['Datetime'].iloc[0], str): - target['Datetime'] = target['Datetime'].apply(lambda x: - datetime.datetime.strptime(x, '%Y-%m-%d %H:%M:%S')) + target['Datetime'] = target['Datetime'].apply( + lambda x: datetime.datetime.strptime(x, '%Y-%m-%d %H:%M:%S') + ) if localtime: - return target[target['Datetime'].dt.date.isin(reference['Datetime'] - .dt.date) & - target['ID'].isin(reference['ID']) & - target[localtime].isin(reference[localtime])] + return target[ + target['Datetime'].dt.date.isin(reference['Datetime'].dt.date) + & target['ID'].isin(reference['ID']) + & target[localtime].isin(reference[localtime]) + ] else: - return target[target['Datetime'].dt.date.isin(reference['Datetime'] - .dt.date) & - target['ID'].isin(reference['ID'])] + return target[ + target['Datetime'].dt.date.isin(reference['Datetime'].dt.date) & + target['ID'].isin(reference['ID']) + ] def concatDelayFiles( - fileList, - sort_list=['ID', 'Datetime'], - return_df=False, - outName=None, - source='model', - ref=None, - col_name='ZTD' + fileList, sort_list=['ID', 'Datetime'], return_df=False, outName=None, source='model', ref=None, col_name='ZTD' ): """ Read a list of .csv files containing the same columns and append them @@ -165,15 +142,11 @@ def concatDelayFiles( dfList[i[0]] = pass_common_obs(dfr, i[1]) del dfr - df_c = pd.concat( - dfList, - ignore_index=True - ).drop_duplicates().reset_index(drop=True) + df_c = pd.concat(dfList, ignore_index=True).drop_duplicates().reset_index(drop=True) df_c.sort_values(by=sort_list, inplace=True) print(f'Total number of rows in the concatenated file: {df_c.shape[0]}') - print(f'Total number of rows containing NaNs: {df_c[df_c.isna().any(axis=1)].shape[0]}' - ) + print(f'Total number of rows containing NaNs: {df_c[df_c.isna().any(axis=1)].shape[0]}') if return_df or outName is None: return df_c @@ -191,41 +164,33 @@ def local_time_filter(raiderFile, ztdFile, dfr, dfz, localTime): localTime_hrthreshold = int(localTime.split(' ')[1]) # with rotation rate and distance to 0 lon, get localtime shift WRT 00 UTC at 0 lon # *rotation rate at given point = (360deg/23.9333333333hr) = 15.041782729825965 deg/hr - dfr['Localtime'] = (dfr['Lon'] / 15.041782729825965) - dfz['Localtime'] = (dfz['Lon'] / 15.041782729825965) + dfr['Localtime'] = dfr['Lon'] / 15.041782729825965 + dfz['Localtime'] = dfz['Lon'] / 15.041782729825965 # estimate local-times - dfr['Localtime'] = dfr.apply(lambda r: update_time(r, localTime_hrs), - axis=1) - dfz['Localtime'] = dfz.apply(lambda r: update_time(r, localTime_hrs), - axis=1) + dfr['Localtime'] = dfr.apply(lambda r: update_time(r, localTime_hrs), axis=1) + dfz['Localtime'] = dfz.apply(lambda r: update_time(r, localTime_hrs), axis=1) # filter out data outside of --localtime hour threshold - dfr['Localtime_u'] = dfr['Localtime'] + \ - datetime.timedelta(hours=localTime_hrthreshold) - dfr['Localtime_l'] = dfr['Localtime'] - \ - datetime.timedelta(hours=localTime_hrthreshold) + dfr['Localtime_u'] = dfr['Localtime'] + datetime.timedelta(hours=localTime_hrthreshold) + dfr['Localtime_l'] = dfr['Localtime'] - datetime.timedelta(hours=localTime_hrthreshold) OG_total = dfr.shape[0] - dfr = dfr[(dfr['Datetime'] >= dfr['Localtime_l']) & - (dfr['Datetime'] <= dfr['Localtime_u'])] + dfr = dfr[(dfr['Datetime'] >= dfr['Localtime_l']) & (dfr['Datetime'] <= dfr['Localtime_u'])] # only keep observation closest to Localtime - print('Total number of datapoints dropped in {} for not being within ' - '{} hrs of specified local-time {}: {} out of {}'.format( - raiderFile, localTime.split(' ')[1], localTime.split(' ')[0], - dfr.shape[0], OG_total)) - dfz['Localtime_u'] = dfz['Localtime'] + \ - datetime.timedelta(hours=localTime_hrthreshold) - dfz['Localtime_l'] = dfz['Localtime'] - \ - datetime.timedelta(hours=localTime_hrthreshold) + print( + f'Total number of datapoints dropped in {raiderFile} for not being within {localTime.split(' ')[1]} hrs of ' + f'specified local-time {localTime.split(' ')[0]}: {dfr.shape[0]} out of {OG_total}' + ) + dfz['Localtime_u'] = dfz['Localtime'] + datetime.timedelta(hours=localTime_hrthreshold) + dfz['Localtime_l'] = dfz['Localtime'] - datetime.timedelta(hours=localTime_hrthreshold) OG_total = dfz.shape[0] - dfz = dfz[(dfz['Datetime'] >= dfz['Localtime_l']) & - (dfz['Datetime'] <= dfz['Localtime_u'])] + dfz = dfz[(dfz['Datetime'] >= dfz['Localtime_l']) & (dfz['Datetime'] <= dfz['Localtime_u'])] # only keep observation closest to Localtime - print('Total number of datapoints dropped in {} for not being within ' - '{} hrs of specified local-time {}: {} out of {}'.format( - ztdFile, localTime.split(' ')[1], localTime.split(' ')[0], - dfz.shape[0], OG_total)) + print( + f'Total number of datapoints dropped in {ztdFile} for not being within {localTime.split(' ')[1]} hrs of ' + f'specified local-time {localTime.split(' ')[0]}: {dfz.shape[0]} out of {OG_total}' + ) # drop all lines with nans dfr.dropna(how='any', inplace=True) @@ -263,44 +228,50 @@ def create_parser(): raiderCombine.py --raiderDir './*' --raider 'combined_raider_delays.csv' raiderCombine.py --raiderDir ERA5/ --raider ERA5_combined_delays.csv --raider_column totalDelay --gnssDir GNSS/ --gnss UNRCombined_gnss.csv --column ZTD -o Combined_delays.csv raiderCombine.py --raiderDir ERA5_2019/ --raider ERA5_combined_delays_2019.csv --raider_column totalDelay --gnssDir GNSS_2019/ --gnss UNRCombined_gnss_2019.csv --column ZTD -o Combined_delays_2019_UTTC18.csv --localtime '18:00:00 1' - """) + """), ) p.add_argument( - '--raider', dest='raider_file', + '--raider', + dest='raider_file', help=dedent("""\ .csv file containing RAiDER-derived Zenith Delays. Should contain columns "ID" and "Datetime" in addition to the delay column If the file does not exist, I will attempt to create it from a directory of delay files. """), - required=True + required=True, ) p.add_argument( - '--raiderDir', '-d', dest='raider_folder', + '--raiderDir', + '-d', + dest='raider_folder', help=dedent("""\ Directory containing RAiDER-derived Zenith Delay files. Files should be named with a Datetime in the name and contain the column "ID" as the delay column names. """), - default=os.getcwd() + default=os.getcwd(), ) p.add_argument( - '--gnssDir', '-gd', dest='gnss_folder', + '--gnssDir', + '-gd', + dest='gnss_folder', help=dedent("""\ Directory containing GNSS-derived Zenith Delay files. Files should contain the column "ID" as the delay column names and times should be denoted by the "Date" key. """), - default=os.getcwd() + default=os.getcwd(), ) p.add_argument( - '--gnss', dest='gnss_file', + '--gnss', + dest='gnss_file', help=dedent("""\ Optional .csv file containing GPS Zenith Delays. Should contain columns "ID", "ZTD", and "Datetime" """), - default=None + default=None, ) p.add_argument( @@ -310,7 +281,7 @@ def create_parser(): help=dedent("""\ Name of the column containing RAiDER delays. Only used with the "--gnss" option """), - default='totalDelay' + default='totalDelay', ) p.add_argument( '--column', @@ -320,7 +291,7 @@ def create_parser(): Name of the column containing GPS Zenith delays. Only used with the "--gnss" option """), - default='ZTD' + default='ZTD', ) p.add_argument( @@ -331,7 +302,7 @@ def create_parser(): Name to use for the combined delay file. Only used with the "--gnss" option """), - default='Combined_delays.csv' + default='Combined_delays.csv', ) p.add_argument( @@ -345,7 +316,7 @@ def create_parser(): Input in 'HH H', e.g. '16 1'" """), - default=None + default=None, ) return p @@ -356,19 +327,25 @@ def main(raiderFile, ztdFile, col_name='ZTD', raider_delay='totalDelay', outName print(f'Merging delay files {raiderFile} and {ztdFile}') dfr = pd.read_csv(raiderFile, parse_dates=['Datetime']) # drop extra columns - expected_data_columns = ['ID', 'Lat', 'Lon', 'Hgt_m', 'Datetime', 'wetDelay', - 'hydroDelay', raider_delay] - dfr = dfr.drop(columns=[col for col in dfr if col not in - expected_data_columns]) + expected_data_columns = ['ID', 'Lat', 'Lon', 'Hgt_m', 'Datetime', 'wetDelay', 'hydroDelay', raider_delay] + dfr = dfr.drop(columns=[col for col in dfr if col not in expected_data_columns]) dfz = pd.read_csv(ztdFile, parse_dates=['Date']) if 'Datetime' not in dfz.keys(): dfz.rename(columns={'Date': 'Datetime'}, inplace=True) # drop extra columns - expected_data_columns = ['ID', 'Datetime', 'wet_delay', 'hydrostatic_delay', - 'times', 'sigZTD', 'Lat', 'Lon', 'Hgt_m', - col_name] - dfz = dfz.drop(columns=[col for col in dfz if col not in - expected_data_columns]) + expected_data_columns = [ + 'ID', + 'Datetime', + 'wet_delay', + 'hydrostatic_delay', + 'times', + 'sigZTD', + 'Lat', + 'Lon', + 'Hgt_m', + col_name, + ] + dfz = dfz.drop(columns=[col for col in dfz if col not in expected_data_columns]) # only pass common locations and times dfz = pass_common_obs(dfr, dfz) dfr = pass_common_obs(dfz, dfr) @@ -392,30 +369,22 @@ def main(raiderFile, ztdFile, col_name='ZTD', raider_delay='totalDelay', outName print('Beginning merge') dfc = dfr.merge( - dfz[common_keys + ['ZTD', 'sigZTD']], - how='left', - left_on=common_keys, - right_on=common_keys, - sort=True + dfz[common_keys + ['ZTD', 'sigZTD']], how='left', left_on=common_keys, right_on=common_keys, sort=True ) # only keep observation closest to Localtime if 'Localtime' in dfc.keys(): - dfc['Localtimediff'] = abs((dfc['Datetime'] - - dfc['Localtime']).dt.total_seconds() / 3600) - dfc = dfc.loc[dfc.groupby(['ID', 'Localtime']).Localtimediff.idxmin() - ].reset_index(drop=True) + dfc['Localtimediff'] = abs((dfc['Datetime'] - dfc['Localtime']).dt.total_seconds() / 3600) + dfc = dfc.loc[dfc.groupby(['ID', 'Localtime']).Localtimediff.idxmin()].reset_index(drop=True) dfc.drop(columns=['Localtimediff'], inplace=True) # estimate residual dfc['ZTD_minus_RAiDER'] = dfc['ZTD'] - dfc[raider_delay] - print('Total number of rows in the concatenated file: ' - f'{dfc.shape[0]}') - print(f'Total number of rows containing NaNs: {dfc[dfc.isna().any(axis=1)].shape[0]}' - ) + print('Total number of rows in the concatenated file: ' f'{dfc.shape[0]}') + print(f'Total number of rows containing NaNs: {dfc[dfc.isna().any(axis=1)].shape[0]}') print('Merge finished') - + if outName is None: return dfc else: diff --git a/tools/RAiDER/interpolator.py b/tools/RAiDER/interpolator.py index b7846778d..a7f1c77b8 100644 --- a/tools/RAiDER/interpolator.py +++ b/tools/RAiDER/interpolator.py @@ -17,14 +17,13 @@ class RegularGridInterpolator: Provides a wrapper around RAiDER.interpolate.interpolate with a similar interface to scipy.interpolate.RegularGridInterpolator. """ - def __init__( self, grid, values, fill_value=None, assume_sorted=False, - max_threads=8 + max_threads=8, ) -> None: self.grid = grid self.values = values @@ -36,7 +35,7 @@ def __call__(self, points): if isinstance(points, tuple): shape = points[0].shape for arr in points: - assert arr.shape == shape, "All dimensions must contain the same number of points!" + assert arr.shape == shape, 'All dimensions must contain the same number of points!' interp_points = np.stack(points, axis=-1) in_shape = interp_points.shape elif points.ndim > 2: @@ -52,7 +51,7 @@ def __call__(self, points): interp_points, fill_value=self.fill_value, assume_sorted=self.assume_sorted, - max_threads=self.max_threads + max_threads=self.max_threads, ) return out.reshape(in_shape[:-1]) @@ -70,8 +69,9 @@ def interp_along_axis(oldCoord, newCoord, data, axis=2, pad=False): stackedData = np.concatenate([oldCoord, data, newCoord], axis=axis) out = np.apply_along_axis(interpVector, axis=axis, arr=stackedData, Nx=oldCoord.shape[axis]) else: - out = np.apply_along_axis(interpV, axis=axis, arr=data, old_x=oldCoord, new_x=newCoord, - left=np.nan, right=np.nan) + out = np.apply_along_axis( + interpV, axis=axis, arr=data, old_x=oldCoord, new_x=newCoord, left=np.nan, right=np.nan + ) return out @@ -88,20 +88,20 @@ def interpVector(vec, Nx): number of original x-points. """ x = vec[:Nx] - y = vec[Nx:2 * Nx] - xnew = vec[2 * Nx:] + y = vec[Nx : 2 * Nx] + xnew = vec[2 * Nx :] f = interp1d(x, y, bounds_error=False, copy=False, assume_sorted=True) return f(xnew) -def fillna3D(array, axis=-1, fill_value=0.): +def fillna3D(array, axis=-1, fill_value=0.0): """ This function fills in NaNs in 3D arrays, specifically using the nearest non-nan value - for "low" NaNs and 0s for "high" NaNs. + for "low" NaNs and 0s for "high" NaNs. Arguments: array - 3D array, where the last axis is the "z" dimension - + Returns: 3D array with low NaNs filled as nearest neighbors and high NaNs filled as 0s """ @@ -124,9 +124,10 @@ def interpolateDEM(demFile, outLL, method='nearest'): For now will only use first row/col of 2D """ import rioxarray as xrr - da_dem = xrr.open_rasterio(demFile, band_as_variable=True)['band_1'] + + da_dem = xrr.open_rasterio(demFile, band_as_variable=True)['band_1'] lats, lons = outLL - lats = lats[:, 0] if lats.ndim==2 else lats - lons = lons[0, :] if lons.ndim==2 else lons + lats = lats[:, 0] if lats.ndim == 2 else lats + lons = lons[0, :] if lons.ndim == 2 else lons z_out = da_dem.interp(y=np.sort(lats)[::-1], x=lons).data - return z_out \ No newline at end of file + return z_out diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index 0c6634dc7..fdecdef55 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -32,30 +32,26 @@ class AOI: _proj - pyproj-compatible CRS _type - Type of AOI """ + def __init__(self) -> None: self._output_directory = os.getcwd() - self._bounding_box = None - self._proj = CRS.from_epsg(4326) - self._geotransform = None + self._bounding_box = None + self._proj = CRS.from_epsg(4326) + self._geotransform = None self._cube_spacing_m = None - def type(self): return self._type - def bounds(self): return list(self._bounding_box).copy() - def geotransform(self): return self._geotransform - def projection(self): return self._proj - def get_output_spacing(self, crs=4326): """Return the output spacing in desired units.""" output_spacing_deg = self._output_spacing @@ -66,11 +62,10 @@ def get_output_spacing(self, crs=4326): if all(axis_info.unit_name == 'degree' for axis_info in crs.axis_info): output_spacing = output_spacing_deg else: - output_spacing = output_spacing_deg*1e5 + output_spacing = output_spacing_deg * 1e5 return output_spacing - def set_output_spacing(self, ll_res=None) -> None: """Calculate the spacing for the output grid and weather model. @@ -79,16 +74,13 @@ def set_output_spacing(self, ll_res=None) -> None: Returns: None. Sets self._output_spacing """ - assert ll_res or self._cube_spacing_m, \ - 'Must pass lat/lon resolution if _cube_spacing_m is None' + assert ll_res or self._cube_spacing_m, 'Must pass lat/lon resolution if _cube_spacing_m is None' - out_spacing = self._cube_spacing_m / 1e5 \ - if self._cube_spacing_m else ll_res + out_spacing = self._cube_spacing_m / 1e5 if self._cube_spacing_m else ll_res logger.debug(f'Output cube spacing: {out_spacing} degrees') self._output_spacing = out_spacing - def add_buffer(self, ll_res, digits=2) -> None: """ Add a fixed buffer to the AOI, accounting for the cube spacing. @@ -115,21 +107,19 @@ def add_buffer(self, ll_res, digits=2) -> None: ## add an extra buffer around the user specified region S, N, W, E = self.bounds() - buffer = (1.5 * ll_res) - S, N = np.max([S-buffer, -90]), np.min([N+buffer, 90]) - W, E = W-buffer, E+buffer # TODO: handle dateline crossings + buffer = 1.5 * ll_res + S, N = np.max([S - buffer, -90]), np.min([N + buffer, 90]) + W, E = W - buffer, E + buffer # TODO: handle dateline crossings ## clip the buffered region to a multiple of the spacing self.set_output_spacing(ll_res) - S, N, W, E = clip_bbox([S,N,W,E], self._output_spacing) + S, N, W, E = clip_bbox([S, N, W, E], self._output_spacing) if np.max([np.abs(W), np.abs(E)]) > 180: logger.warning('Bounds extend past +/- 180. Results may be incorrect.') self._bounding_box = [np.round(a, digits) for a in (S, N, W, E)] - return - def calc_buffer_ray(self, direction, lookDir='right', incAngle=30, maxZ=80, digits=2): """ @@ -149,20 +139,18 @@ def calc_buffer_ray(self, direction, lookDir='right', incAngle=30, maxZ=80, digi except AttributeError: lookDir = lookDir.lower() - assert direction in 'asc desc'.split(), \ - f'Incorrection orbital direction: {direction}. Choose asc or desc.' - assert lookDir in 'right light'.split(), \ - f'Incorrection look direction: {lookDir}. Choose right or left.' + assert direction in 'asc desc'.split(), f'Incorrection orbital direction: {direction}. Choose asc or desc.' + assert lookDir in 'right light'.split(), f'Incorrection look direction: {lookDir}. Choose right or left.' S, N, W, E = self.bounds() # use a small look angle to calculate near range lat_max = np.max([np.abs(S), np.abs(N)]) - near = maxZ * np.tan(np.deg2rad(incAngle)) - buffer = near / (np.cos(np.deg2rad(lat_max)) * 100) + near = maxZ * np.tan(np.deg2rad(incAngle)) + buffer = near / (np.cos(np.deg2rad(lat_max)) * 100) # buffer on the side nearest the sensor - if ((lookDir == 'right') and (direction == 'asc')) or ((lookDir == 'left') and (direction == 'desc')): + if (lookDir == 'right' and direction == 'asc') or (lookDir == 'left' and direction == 'desc'): W = W - buffer else: E = E + buffer @@ -172,11 +160,9 @@ def calc_buffer_ray(self, direction, lookDir='right', incAngle=30, maxZ=80, digi logger.warning('Bounds extend past +/- 180. Results may be incorrect.') return bounds - def set_output_directory(self, output_directory) -> None: self._output_directory = output_directory - def set_output_xygrid(self, dst_crs=4326) -> None: """Define the locations where the delays will be returned.""" from RAiDER.utilFcns import transform_bbox @@ -189,52 +175,52 @@ def set_output_xygrid(self, dst_crs=4326) -> None: except pyproj.exceptions.CRSError: out_proj = dst_crs - out_snwe = transform_bbox(self.bounds(), src_crs=4326, dest_crs=out_proj) - logger.debug(f"Output SNWE: {out_snwe}") + logger.debug(f'Output SNWE: {out_snwe}') # Build the output grid out_spacing = self.get_output_spacing(out_proj) self.xpts = np.arange(out_snwe[2], out_snwe[3] + out_spacing, out_spacing) self.ypts = np.arange(out_snwe[1], out_snwe[0] - out_spacing, -out_spacing) - return class StationFile(AOI): """Use a .csv file containing at least Lat, Lon, and optionally Hgt_m columns.""" + def __init__(self, station_file, demFile=None) -> None: super().__init__() self._filename = station_file - self._demfile = demFile + self._demfile = demFile self._bounding_box = bounds_from_csv(station_file) self._type = 'station_file' - def readLL(self): """Read the station lat/lons from the csv file.""" - df = pd.read_csv(self._filename).drop_duplicates(subset=["Lat", "Lon"]) + df = pd.read_csv(self._filename).drop_duplicates(subset=['Lat', 'Lon']) return df['Lat'].values, df['Lon'].values - def readZ(self): """Read the station heights from the file, or download a DEM if not present.""" - df = pd.read_csv(self._filename).drop_duplicates(subset=["Lat", "Lon"]) + df = pd.read_csv(self._filename).drop_duplicates(subset=['Lat', 'Lon']) if 'Hgt_m' in df.columns: return df['Hgt_m'].values else: # Download the DEM from RAiDER.dem import download_dem from RAiDER.interpolator import interpolateDEM - - demFile = os.path.join(self._output_directory, 'GLO30_fullres_dem.tif') \ - if self._demfile is None else self._demfile - _, _ = download_dem( + demFile = ( + os.path.join(self._output_directory, 'GLO30_fullres_dem.tif') + if self._demfile is None + else self._demfile + ) + + download_dem( self._bounding_box, writeDEM=True, demName=demFile, ) - + ## interpolate the DEM to the query points z_out0 = interpolateDEM(demFile, self.readLL()) if np.isnan(z_out0).all(): @@ -249,7 +235,8 @@ def readZ(self): class RasterRDR(AOI): - """Use a 2-band raster file containing lat/lon coordinates.""" + """Use a 2-band raster file containing lat/lon coordinates.""" + def __init__(self, lat_file, lon_file=None, hgt_file=None, dem_file=None, convention='isce') -> None: super().__init__() self._type = 'radar_rasters' @@ -273,7 +260,6 @@ def __init__(self, lat_file, lon_file=None, hgt_file=None, dem_file=None, conven self._demfile = dem_file self._convention = convention - def readLL(self): # allow for 2-band lat/lon raster lats = rio_open(self._latfile) @@ -283,7 +269,6 @@ def readLL(self): else: return lats, rio_open(self._lonfile) - def readZ(self): """Read the heights from the raster file, or download a DEM if not present.""" if self._hgtfile is not None and os.path.exists(self._hgtfile): @@ -294,11 +279,14 @@ def readZ(self): # Download the DEM from RAiDER.dem import download_dem from RAiDER.interpolator import interpolateDEM - - demFile = os.path.join(self._output_directory, 'GLO30_fullres_dem.tif') \ - if self._demfile is None else self._demfile - _, _ = download_dem( + demFile = ( + os.path.join(self._output_directory, 'GLO30_fullres_dem.tif') + if self._demfile is None + else self._demfile + ) + + download_dem( self._bounding_box, writeDEM=True, demName=demFile, @@ -310,6 +298,7 @@ def readZ(self): class BoundingBox(AOI): """Parse a bounding box AOI.""" + def __init__(self, bbox) -> None: AOI.__init__(self) self._bounding_box = bbox @@ -318,15 +307,16 @@ def __init__(self, bbox) -> None: class GeocodedFile(AOI): """Parse a Geocoded file for coordinates.""" + def __init__(self, filename, is_dem=False) -> None: super().__init__() from RAiDER.utilFcns import rio_extents, rio_profile - self._filename = filename - self.p = rio_profile(filename) + self._filename = filename + self.p = rio_profile(filename) self._bounding_box = rio_extents(self.p) - self._is_dem = is_dem + self._is_dem = is_dem _, self._proj, self._geotransform = rio_stats(filename) self._type = 'geocoded_file' try: @@ -334,18 +324,16 @@ def __init__(self, filename, is_dem=False) -> None: except KeyError: self.crs = None - def readLL(self): # ll_bounds are SNWE S, N, W, E = self._bounding_box w, h = self.p['width'], self.p['height'] - px = (E - W) / w - py = (N - S) / h + px = (E - W) / w + py = (N - S) / h x = np.array([W + (t * px) for t in range(w)]) y = np.array([S + (t * py) for t in range(h)]) - X, Y = np.meshgrid(x,y) - return Y, X # lats, lons - + X, Y = np.meshgrid(x, y) + return Y, X # lats, lons def readZ(self): """Download a DEM for the file.""" @@ -353,7 +341,7 @@ def readZ(self): from RAiDER.interpolator import interpolateDEM demFile = self._filename if self._is_dem else 'GLO30_fullres_dem.tif' - bbox = self._bounding_box + bbox = self._bounding_box _, _ = download_dem(bbox, writeDEM=True, demName=demFile) z_out = interpolateDEM(demFile, self.readLL()) @@ -362,9 +350,10 @@ def readZ(self): class Geocube(AOI): """Pull lat/lon/height from a georeferenced data cube.""" + def __init__(self, path_cube) -> None: super().__init__() - self.path = path_cube + self.path = path_cube self._type = 'Geocube' self._bounding_box = self.get_extent() _, self._proj, self._geotransform = rio_stats(path_cube) @@ -395,6 +384,7 @@ def bounds_from_latlon_rasters(latfile, lonfile): the appropriate outputs. """ from RAiDER.utilFcns import get_file_and_band + latinfo = get_file_and_band(latfile) loninfo = get_file_and_band(lonfile) lat_stats, lat_proj, lat_gt = rio_stats(latinfo[0], band=latinfo[1]) @@ -407,8 +397,7 @@ def bounds_from_latlon_rasters(latfile, lonfile): raise ValueError('Affine transform for Latitude and Longitude files does not match') # TODO - handle dateline crossing here - snwe = (lat_stats.min, lat_stats.max, - lon_stats.min, lon_stats.max) + snwe = (lat_stats.min, lat_stats.max, lon_stats.min, lon_stats.max) if lat_proj is None: logger.debug('Assuming lat/lon files are in EPSG:4326') @@ -422,6 +411,6 @@ def bounds_from_csv(station_file): station_file should be a comma-delimited file with at least "Lat" and "Lon" columns, which should be EPSG: 4326 projection (i.e WGS84). """ - stats = pd.read_csv(station_file).drop_duplicates(subset=["Lat", "Lon"]) + stats = pd.read_csv(station_file).drop_duplicates(subset=['Lat', 'Lon']) snwe = [stats['Lat'].min(), stats['Lat'].max(), stats['Lon'].min(), stats['Lon'].max()] return snwe diff --git a/tools/RAiDER/logger.py b/tools/RAiDER/logger.py index 4ed748420..3f5b36a9b 100644 --- a/tools/RAiDER/logger.py +++ b/tools/RAiDER/logger.py @@ -6,6 +6,7 @@ # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """Global logging configuration.""" + import logging import os import sys @@ -17,18 +18,14 @@ # Inspired by # https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output class UnixColorFormatter(Formatter): - yellow = "\x1b[33;21m" - red = "\x1b[31;21m" - bold_red = "\x1b[31;1m" - reset = "\x1b[0m" - - COLORS = { - logging.WARNING: yellow, - logging.ERROR: red, - logging.CRITICAL: bold_red - } - - def __init__(self, fmt=None, datefmt=None, style="%", use_color=True) -> None: + yellow = '\x1b[33;21m' + red = '\x1b[31;21m' + bold_red = '\x1b[31;1m' + reset = '\x1b[0m' + + COLORS = {logging.WARNING: yellow, logging.ERROR: red, logging.CRITICAL: bold_red} + + def __init__(self, fmt=None, datefmt=None, style='%', use_color=True) -> None: super().__init__(fmt, datefmt, style) # Save the old function so we can call it later self.__formatMessage = self.formatMessage @@ -39,7 +36,7 @@ def formatMessageColor(self, record): message = self.__formatMessage(record) color = self.COLORS.get(record.levelno) if color: - message = "".join([color, message, self.reset]) + message = ''.join([color, message, self.reset]) return message @@ -49,36 +46,34 @@ class CustomFormatter(UnixColorFormatter): def formatMessage(self, record): message = super().formatMessage(record) if record.levelno >= logging.WARNING: - message = ": ".join((record.levelname, message)) + message = ': '.join((record.levelname, message)) return message ##################################### -## DEFINE THE LOGGER +# DEFINE THE LOGGER if conf.LOGGER_PATH is None: logger_path = os.getcwd() else: logger_path = conf.LOGGER_PATH -logger = logging.getLogger("RAiDER") +logger = logging.getLogger('RAiDER') logger.setLevel(logging.DEBUG) stdout_handler = StreamHandler(sys.stdout) -stdout_handler.setFormatter(CustomFormatter(use_color=os.name != "nt")) +stdout_handler.setFormatter(CustomFormatter(use_color=os.name != 'nt')) stdout_handler.setLevel(logging.DEBUG) -debugfile_handler = FileHandler(os.path.join(logger_path, "debug.log")) -debugfile_handler.setFormatter(Formatter( - "[{asctime}] {levelname:<10} {module} {exc_info} {funcName:>20}:{lineno:<5} {message}", - style="{" -)) +debugfile_handler = FileHandler(os.path.join(logger_path, 'debug.log')) +debugfile_handler.setFormatter( + Formatter('[{asctime}] {levelname:<10} {module} {exc_info} {funcName:>20}:{lineno:<5} {message}', style='{') +) debugfile_handler.setLevel(logging.DEBUG) -errorfile_handler = FileHandler(os.path.join(logger_path, "error.log")) -errorfile_handler.setFormatter(Formatter( - "[{asctime}] {levelname:<10} {module:<10} {exc_info} {funcName:>20}:{lineno:<5} {message}", - style="{" -)) +errorfile_handler = FileHandler(os.path.join(logger_path, 'error.log')) +errorfile_handler.setFormatter( + Formatter('[{asctime}] {levelname:<10} {module:<10} {exc_info} {funcName:>20}:{lineno:<5} {message}', style='{') +) # , , , , , , errorfile_handler.setLevel(logging.WARNING) diff --git a/tools/RAiDER/losreader.py b/tools/RAiDER/losreader.py index 271d6344d..07b88fc3c 100644 --- a/tools/RAiDER/losreader.py +++ b/tools/RAiDER/losreader.py @@ -31,6 +31,7 @@ class LOS(ABC): """LOS Class definition for handling look vectors.""" + def __init__(self) -> None: self._lats, self._lons, self._heights = None, None, None self._look_vecs = None @@ -38,7 +39,6 @@ def __init__(self) -> None: self._is_zenith = False self._is_projected = False - def setPoints(self, lats, lons=None, heights=None) -> None: """Set the pixel locations.""" if (lats is None) and (self._lats is None): @@ -59,30 +59,26 @@ def setPoints(self, lats, lons=None, heights=None) -> None: self._lons = lons self._heights = heights - def setTime(self, dt) -> None: self._time = dt - def is_Zenith(self): return self._is_zenith - def is_Projected(self): return self._is_projected - def ray_trace(self): return self._ray_trace class Zenith(LOS): """Class definition for a "Zenith" object.""" + def __init__(self) -> None: super().__init__() self._is_zenith = True - def setLookVectors(self) -> None: """Set point locations and calculate Zenith look vectors.""" if self._lats is None: @@ -90,7 +86,6 @@ def setLookVectors(self) -> None: if self._look_vecs is None: self._look_vecs = getZenithLookVecs(self._lats, self._lons, self._heights) - def __call__(self, delays): """Placeholder method for consistency with the other classes.""" return delays @@ -101,17 +96,17 @@ class Conventional(LOS): Special value indicating that the zenith delay will be projected using the standard cos(inc) scaling. """ + def __init__(self, filename=None, los_convention='isce', time=None, pad=600) -> None: super().__init__() self._file = filename self._time = time - self._pad = pad + self._pad = pad self._is_projected = True - self._convention = los_convention + self._convention = los_convention if self._convention.lower() != 'isce': raise NotImplementedError() - def __call__(self, delays): """Read the LOS file and convert it to look vectors.""" if self._lats is None: @@ -125,12 +120,11 @@ def __call__(self, delays): except (OSError, TypeError): # Otherwise, treat it as an orbit / statevector file - svs = np.stack( - get_sv(self._file, self._time, self._pad), axis=-1 + svs = np.stack(get_sv(self._file, self._time, self._pad), axis=-1) + LOS_enu = state_to_los( + svs, + [self._lats, self._lons, self._heights], ) - LOS_enu = state_to_los(svs, - [self._lats, self._lons, self._heights], - ) if delays.shape == LOS_enu.shape: return delays / LOS_enu @@ -176,7 +170,7 @@ class Raytracing(LOS): >>> import numpy as np """ - def __init__(self, filename=None, los_convention='isce', time=None, look_dir = 'right', pad=600) -> None: + def __init__(self, filename=None, los_convention='isce', time=None, look_dir='right', pad=600) -> None: """Read in and parse a statevector file.""" if isce is None: raise ImportError('isce3 is required for this class. Use conda to install isce3`') @@ -185,7 +179,7 @@ def __init__(self, filename=None, los_convention='isce', time=None, look_dir = ' self._ray_trace = True self._file = filename self._time = time - self._pad = pad + self._pad = pad self._convention = los_convention self._orbit = None if self._convention.lower() != 'isce': @@ -197,26 +191,21 @@ def __init__(self, filename=None, los_convention='isce', time=None, look_dir = ' self._orbit = get_orbit(self._file, self._time, pad=pad) self._elp = isce.core.Ellipsoid() self._dop = isce.core.LUT2d() - if look_dir.lower() == "right": + if look_dir.lower() == 'right': self._look_dir = isce.core.LookSide.Right - elif look_dir.lower() == "left": + elif look_dir.lower() == 'left': self._look_dir = isce.core.LookSide.Left else: - raise RuntimeError(f"Unknown look direction: {look_dir}") - + raise RuntimeError(f'Unknown look direction: {look_dir}') def getSensorDirection(self) -> Literal['desc', 'asc']: if self._orbit is None: raise ValueError('The orbit has not been set') - z = self._orbit.position[:,2] + z = self._orbit.position[:, 2] t = self._orbit.time start = np.argmin(t) end = np.argmax(t) - if z[start] > z[end]: - return 'desc' - else: - return 'asc' - + return 'desc' if z[start] > z[end] else 'asc' def getLookDirection(self): return self._look_dir @@ -226,7 +215,6 @@ def setTime(self, time, pad=600) -> None: self._time = time self._orbit = get_orbit(self._file, self._time, pad=pad) - def getLookVectors(self, ht, llh, xyz, yy): """Calculate look vectors for raytracing.""" if isce is None: @@ -249,17 +237,22 @@ def getLookVectors(self, ht, llh, xyz, yy): # Wavelength does not matter for try: aztime, slant_range = isce.geometry.geo2rdr( - inp, self._elp, self._orbit, self._dop, 0.06, self._look_dir, + inp, + self._elp, + self._orbit, + self._dop, + 0.06, + self._look_dir, threshold=1.0e-7, maxiter=30, - delta_range=10.0) + delta_range=10.0, + ) sat_xyz, _ = self._orbit.interpolate(aztime) los[ii, jj, :] = (sat_xyz - inp_xyz) / slant_range except Exception: los[ii, jj, :] = np.nan return los - def getIntersectionWithHeight(self, height): """ This function computes the intersection point of a ray at a height @@ -268,7 +261,6 @@ def getIntersectionWithHeight(self, height): # We just leverage the same code as finding top of atmosphere here return getTopOfAtmosphere(self._xyz, self._look_vecs, height) - def getIntersectionWithLevels(self, levels): """ This function returns the points at which rays intersect the @@ -294,7 +286,6 @@ def getIntersectionWithLevels(self, levels): return rays - def calculateDelays(self, delays) -> NoReturn: """ Here "delays" is point-wise delays (i.e. refractivities), not @@ -324,9 +315,7 @@ def getZenithLookVecs(lats, lons, heights): return np.stack([x, y, z], axis=-1) -def get_sv(los_file: Union[str, list, PosixPath], - ref_time: datetime.datetime, - pad: int): +def get_sv(los_file: Union[str, list, PosixPath], ref_time: datetime.datetime, pad: int): """ Read an LOS file and return orbital state vectors. @@ -358,6 +347,7 @@ def get_sv(los_file: Union[str, list, PosixPath], def filter_ESA_orbit_file_p(path: str) -> bool: return filter_ESA_orbit_file(path, ref_time) + los_files = list(filter(filter_ESA_orbit_file_p, los_files)) if not los_files: raise ValueError('There are no valid orbit files provided') @@ -369,13 +359,10 @@ def filter_ESA_orbit_file_p(path: str) -> bool: try: svs = read_shelve(los_file) except BaseException: - raise ValueError( - f'get_sv: I cannot parse the statevector file {los_file}' - ) + raise ValueError(f'get_sv: I cannot parse the statevector file {los_file}') except: raise ValueError(f'get_sv: I cannot parse the statevector file {los_file}') - if ref_time: idx = cut_times(svs[0], ref_time, pad=pad) svs = [d[idx] for d in svs] @@ -469,9 +456,10 @@ def read_txt_file(filename): x_, y_, z_, vx_, vy_, vz_ = (float(t) for t in parts[1:]) except ValueError: raise ValueError( - f"I need {filename} to be a 7 column text file, with " + - "columns t, x, y, z, vx, vy, vz (Couldn't parse line " + - f"{repr(line)})") + f'I need {filename} to be a 7 column text file, with ' + + "columns t, x, y, z, vx, vy, vz (Couldn't parse line " + + f'{repr(line)})' + ) t.append(t_) x.append(x_) y.append(y_) @@ -517,12 +505,7 @@ def read_ESA_Orbit_file(filename): vz = np.ones(numOSV) for i, st in enumerate(data_block[0]): - t.append( - datetime.datetime.strptime( - st[1].text, - 'UTC=%Y-%m-%dT%H:%M:%S.%f' - ) - ) + t.append(datetime.datetime.strptime(st[1].text, 'UTC=%Y-%m-%dT%H:%M:%S.%f')) x[i] = float(st[4].text) y[i] = float(st[5].text) @@ -534,11 +517,11 @@ def read_ESA_Orbit_file(filename): return [t, x, y, z, vx, vy, vz] -def pick_ESA_orbit_file(list_files:list, ref_time:datetime.datetime): +def pick_ESA_orbit_file(list_files: list, ref_time: datetime.datetime): """From list of .EOF orbit files, pick the one that contains 'ref_time'.""" orb_file = None for path in list_files: - f = os.path.basename(path) + f = os.path.basename(path) t0 = datetime.datetime.strptime(f.split('_')[6].lstrip('V'), '%Y%m%dT%H%M%S') t1 = datetime.datetime.strptime(f.split('_')[7].rstrip('.EOF'), '%Y%m%dT%H%M%S') if t0 < ref_time < t1: @@ -550,8 +533,7 @@ def pick_ESA_orbit_file(list_files:list, ref_time:datetime.datetime): return path -def filter_ESA_orbit_file(orbit_xml: str, - ref_time: datetime.datetime) -> bool: +def filter_ESA_orbit_file(orbit_xml: str, ref_time: datetime.datetime) -> bool: """Returns true or false depending on whether orbit file contains ref time. Parameters @@ -568,7 +550,7 @@ def filter_ESA_orbit_file(orbit_xml: str, f = os.path.basename(orbit_xml) t0 = datetime.datetime.strptime(f.split('_')[6].lstrip('V'), '%Y%m%dT%H%M%S') t1 = datetime.datetime.strptime(f.split('_')[7].rstrip('.EOF'), '%Y%m%dT%H%M%S') - return (t0 < ref_time < t1) + return t0 < ref_time < t1 ############################ @@ -604,10 +586,7 @@ def state_to_los(svs, llh_targets): # check the inputs if np.min(svs.shape) < 4: - raise RuntimeError( - 'state_to_los: At least 4 state vectors are required' - ' for orbit interpolation' - ) + raise RuntimeError('state_to_los: At least 4 state vectors are required for orbit interpolation') # Convert svs to isce3 orbit orb = isce.core.Orbit([ @@ -618,7 +597,7 @@ def state_to_los(svs, llh_targets): ]) # Flatten the input array for convenience - in_shape = llh_targets[0].shape + in_shape = llh_targets[0].shape target_llh = np.stack([x.flatten() for x in llh_targets], axis=-1) # Iterate through targets and compute LOS @@ -643,9 +622,7 @@ def cut_times(times, ref_time, pad): ------- idx: Nt x 1 logical ndarray - a mask of times within the padded request time. """ - diff = np.array( - [(x - ref_time).total_seconds() for x in times] - ) + diff = np.array([(x - ref_time).total_seconds() for x in times]) return np.abs(diff) < pad @@ -671,9 +648,7 @@ def get_radar_pos(llh, orb): residual_threshold = 1.0e-7 # Get xyz positions of targets here from lat/lon/height - targ_xyz = np.stack( - lla2ecef(llh[:, 0], llh[:, 1], llh[:, 2]), axis=-1 - ) + targ_xyz = np.stack(lla2ecef(llh[:, 0], llh[:, 1], llh[:, 2]), axis=-1) # Get some isce3 constants for this inversion # TODO - Assuming right-looking for now @@ -689,31 +664,32 @@ def get_radar_pos(llh, orb): for ind, pt in enumerate(llh): if not any(np.isnan(pt)): # ISCE3 always uses xy convention - inp = np.array([np.deg2rad(pt[1]), - np.deg2rad(pt[0]), - pt[2]]) + inp = np.array([np.deg2rad(pt[1]), np.deg2rad(pt[0]), pt[2]]) # Local normal vector nv = elp.n_vector(inp[0], inp[1]) # Wavelength does not matter for zero doppler try: aztime, slant_range = isce.geometry.geo2rdr( - inp, elp, orb, dop, 0.06, look, + inp, + elp, + orb, + dop, + 0.06, + look, threshold=residual_threshold, maxiter=num_iteration, - delta_range=10.0) + delta_range=10.0, + ) sat_xyz, _ = orb.interpolate(aztime) sr[ind] = slant_range - delta = sat_xyz - targ_xyz[ind, :] # TODO - if we only ever need cos(lookang), # skip the arccos here and cos above delta = delta / np.linalg.norm(delta) - output[ind] = np.rad2deg( - np.arccos(np.dot(delta, nv)) - ) + output[ind] = np.rad2deg(np.arccos(np.dot(delta, nv))) except Exception as e: raise e @@ -744,21 +720,19 @@ def getTopOfAtmosphere(xyz, look_vecs, toaheight, factor=None): maxIter = 3 else: maxIter = 10 - factor = 1. + factor = 1.0 # Guess top point pos = xyz + toaheight * look_vecs for _ in range(maxIter): pos_llh = ecef2lla(pos[..., 0], pos[..., 1], pos[..., 2]) - pos = pos + look_vecs * ((toaheight - pos_llh[2])/factor)[..., None] + pos = pos + look_vecs * ((toaheight - pos_llh[2]) / factor)[..., None] return pos -def get_orbit(orbit_file: Union[list, str], - ref_time: datetime.datetime, - pad: int): +def get_orbit(orbit_file: Union[list, str], ref_time: datetime.datetime, pad: int): """ Returns state vectors from an orbit file; state vectors are unique and ordered in terms of time orbit file (str | list): - user-passed file(s) containing statevectors @@ -807,7 +781,7 @@ def build_ray(model_zs, ht, xyz, LOS, MAX_TROPO_HEIGHT=_ZREF): cos_factor = None ray_lengths, low_xyzs, high_xyzs = [], [], [] - for zz in range(model_zs.size-1): + for zz in range(model_zs.size - 1): # Low and High for model interval low_ht = model_zs[zz] high_ht = model_zs[zz + 1] @@ -843,7 +817,7 @@ def build_ray(model_zs, ht, xyz, LOS, MAX_TROPO_HEIGHT=_ZREF): high_xyz = getTopOfAtmosphere(xyz, LOS, high_ht, factor=cos_factor) # Compute ray length - ray_length = np.linalg.norm(high_xyz - low_xyz, axis=-1) + ray_length = np.linalg.norm(high_xyz - low_xyz, axis=-1) # Compute cos_factor for first iteration if cos_factor is None: @@ -853,7 +827,7 @@ def build_ray(model_zs, ht, xyz, LOS, MAX_TROPO_HEIGHT=_ZREF): low_xyzs.append(low_xyz) high_xyzs.append(high_xyz) - ## if all weather model levels are requested the top most layer might not contribute anything + # if all weather model levels are requested the top most layer might not contribute anything if not ray_lengths: return None, None, None else: diff --git a/tools/RAiDER/models/credentials.py b/tools/RAiDER/models/credentials.py index a0f8c2d20..3eee8576f 100644 --- a/tools/RAiDER/models/credentials.py +++ b/tools/RAiDER/models/credentials.py @@ -25,7 +25,7 @@ 'HRES': 'ecmwfapirc', 'GMAO': 'netrc', 'MERRA2': 'netrc', - 'HRRR': None + 'HRRR': None, } APIS = { @@ -35,7 +35,7 @@ 'key: {uid}:{key}\n' ), 'help_url': 'https://cds.climate.copernicus.eu/api-how-to', - 'default_host': 'https://cds.climate.copernicus.eu/api/v2' + 'default_host': 'https://cds.climate.copernicus.eu/api/v2', }, 'ecmwfapirc': { 'template': ( @@ -46,7 +46,7 @@ '}}\n' ), 'help_url': 'https://confluence.ecmwf.int/display/WEBAPI/Access+ECMWF+Public+Datasets#AccessECMWFPublicDatasets-key', - 'default_host': 'https://api.ecmwf.int/v1' + 'default_host': 'https://api.ecmwf.int/v1', }, 'netrc': { 'template': ( @@ -55,8 +55,8 @@ ' password {key}\n' ), 'help_url': 'https://wiki.earthdata.nasa.gov/display/EL/How+To+Access+Data+With+cURL+And+Wget', - 'default_host': 'urs.earthdata.nasa.gov' - } + 'default_host': 'urs.earthdata.nasa.gov', + }, } @@ -69,8 +69,7 @@ def _get_envs(model: str) -> Tuple[Optional[str], Optional[str], Optional[str]]: elif model == 'HRES': uid = os.getenv('RAIDER_HRES_EMAIL') key = os.getenv('RAIDER_HRES_API_KEY') - host = os.getenv('RAIDER_HRES_URL', - APIS['ecmwfapirc']['default_host']) + host = os.getenv('RAIDER_HRES_URL', APIS['ecmwfapirc']['default_host']) elif model in ('GMAO', 'MERRA2'): # same as in DockerizedTopsApp uid = os.getenv('EARTHDATA_USERNAME') @@ -81,11 +80,13 @@ def _get_envs(model: str) -> Tuple[Optional[str], Optional[str], Optional[str]]: return uid, key, host -def check_api(model: str, - uid: Optional[str] = None, - key: Optional[str] = None, - output_dir: str = '~/', - update_rc_file: bool = False) -> None: +def check_api( + model: str, + uid: Optional[str] = None, + key: Optional[str] = None, + output_dir: str = '~/', + update_rc_file: bool = False, +) -> None: # Weather model API RC filename # Typically stored in home dir as a hidden file rc_filename = RC_FILENAMES[model] @@ -96,7 +97,7 @@ def check_api(model: str, return # Get the target rc file's path - hidden_ext = '_' if system() == "Windows" else '.' + hidden_ext = '_' if system() == 'Windows' else '.' rc_path = Path(output_dir) / (hidden_ext + rc_filename) rc_path = rc_path.expanduser() @@ -118,19 +119,19 @@ def check_api(model: str, raise ValueError( f'ERROR: {model} API UID not provided in RAiDER arguments and ' 'not present in environment variables.\n' - f'See info for this model\'s API at \033[1m{help_url}\033[0m' + f"See info for this model's API at \033[1m{help_url}\033[0m" ) elif uid is not None and key is None: raise ValueError( f'ERROR: {model} API key not provided in RAiDER arguments and ' 'not present in environment variables.\n' - f'See info for this model\'s API at \033[1m{help_url}\033[0m' + f"See info for this model's API at \033[1m{help_url}\033[0m" ) else: raise ValueError( f'ERROR: {model} API credentials not provided in RAiDER ' 'arguments and not present in environment variables.\n' - f'See info for this model\'s API at \033[1m{help_url}\033[0m' + f"See info for this model's API at \033[1m{help_url}\033[0m" ) # Create file with the API credentials @@ -150,6 +151,7 @@ def check_api(model: str, # so extra care needs to be taken to make sure we only touch the # one that belongs to this URL. import netrc + rc_path.touch() netrc_credentials = netrc.netrc(rc_path) netrc_credentials.hosts[url] = (uid, None, key) diff --git a/tools/RAiDER/models/customExceptions.py b/tools/RAiDER/models/customExceptions.py index 6c7f29e5b..4bf9dc7f0 100644 --- a/tools/RAiDER/models/customExceptions.py +++ b/tools/RAiDER/models/customExceptions.py @@ -1,47 +1,48 @@ class DatetimeFailed(Exception): def __init__(self, model, time) -> None: - msg = f"Weather model {model} failed to download for datetime {time}" + msg = f'Weather model {model} failed to download for datetime {time}' super().__init__(msg) class DatetimeNotAvailable(Exception): def __init__(self, model, time) -> None: - msg = f"Weather model {model} was not found for datetime {time}" + msg = f'Weather model {model} was not found for datetime {time}' super().__init__(msg) class DatetimeOutsideRange(Exception): def __init__(self, model, time) -> None: - msg = f"Time {time} is outside the available date range for weather model {model}" + msg = f'Time {time} is outside the available date range for weather model {model}' super().__init__(msg) class ExistingWeatherModelTooSmall(Exception): def __init__(self) -> None: - msg = 'The weather model passed does not cover all of the input ' \ - 'points; you may need to download a larger area.' + msg = 'The weather model passed does not cover all of the input points; you may need to download a larger area.' super().__init__(msg) class TryToKeepGoingError(Exception): def __init__(self, date=None) -> None: if date is not None: - msg = 'The weather model does not exist for date {date}, so I will try to use the closest available date.' + msg = f'The weather model does not exist for date {date}, so I will try to use the closest available date.' else: msg = 'I will try to keep going' super().__init__(msg) - + + class CriticalError(Exception): def __init__(self) -> None: msg = 'I have experienced a critical error, please take a look at the log files' super().__init__(msg) + class WrongNumberOfFiles(Exception): def __init__(self, Nexp, Navail) -> None: msg = 'The number of files downloaded does not match the requested, ' f'I expected {Nexp} and got {Navail}, aborting' super().__init__(msg) - + class NoWeatherModelData(Exception): def __init__(self, custom_msg=None) -> None: @@ -54,13 +55,12 @@ def __init__(self, custom_msg=None) -> None: class NoStationDataFoundError(Exception): def __init__(self, station_list=None, years=None) -> None: - if (station_list is None) and (years is None): + if station_list is None and years is None: msg = 'No GNSS station data was found' - elif (years is None): + elif years is None: msg = f'No data was found for GNSS stations {station_list}' elif station_list is None: msg = f'No data was found for years {years}' else: msg = f'No data was found for GNSS stations {station_list} and years {years}' - super().__init__(msg) diff --git a/tools/RAiDER/models/ecmwf.py b/tools/RAiDER/models/ecmwf.py index 7468ed973..d53d168de 100755 --- a/tools/RAiDER/models/ecmwf.py +++ b/tools/RAiDER/models/ecmwf.py @@ -23,8 +23,8 @@ def __init__(self) -> None: WeatherModel.__init__(self) # model constants - self._k1 = 0.776 # [K/Pa] - self._k2 = 0.233 # [K/Pa] + self._k1 = 0.776 # [K/Pa] + self._k2 = 0.233 # [K/Pa] self._k3 = 3.75e3 # [K^2/Pa] self._time_res = TIME_RES['ECMWF'] @@ -35,19 +35,16 @@ def __init__(self) -> None: self._model_level_type = 'ml' # Default - def __pressure_levels__(self): self._zlevels = np.flipud(LEVELS_25_HEIGHTS) - self._levels = len(self._zlevels) - + self._levels = len(self._zlevels) def __model_levels__(self): - self._levels = 137 + self._levels = 137 self._zlevels = np.flipud(LEVELS_137_HEIGHTS) self._a = A_137_HRES self._b = B_137_HRES - def load_weather(self, f=None, *args, **kwargs) -> None: """ Consistent class method to be implemented across all weather model types. @@ -55,16 +52,12 @@ def load_weather(self, f=None, *args, **kwargs) -> None: t, wet_refractivity, hydrostatic refractivity, e) should be fully populated. """ - f = self.files[0] if f is None else f + f = f if f is not None else self.files[0] self._load_model_level(f) - def _load_model_level(self, fname) -> None: # read data from netcdf file - lats, lons, xs, ys, t, q, lnsp, z = self._makeDataCubes( - fname, - verbose=False - ) + lats, lons, xs, ys, t, q, lnsp, z = self._makeDataCubes(fname, verbose=False) # ECMWF appears to give me this backwards if lats[0] > lats[1]: @@ -117,28 +110,14 @@ def _load_model_level(self, fname) -> None: self._xs = self._lons.copy() self._zs = np.flip(h, axis=2) - def _fetch(self, out) -> None: """Fetch a weather model from ECMWF.""" # bounding box plus a buffer lat_min, lat_max, lon_min, lon_max = self._ll_bounds - # execute the search at ECMWF - self._get_from_ecmwf( - lat_min, - lat_max, - self._lat_res, - lon_min, - lon_max, - self._lon_res, - self._time, - out - ) - return + self._get_from_ecmwf(lat_min, lat_max, self._lat_res, lon_min, lon_max, self._lon_res, self._time, out) - - def _get_from_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, - lon_step, time, out) -> None: + def _get_from_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step, time, out) -> None: import ecmwfapi server = ecmwfapi.ECMWFDataServer() @@ -147,54 +126,48 @@ def _get_from_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, if not corrected_DT == time: logger.warning('Rounded given datetime from %s to %s', time, corrected_DT) - server.retrieve({ - "class": self._classname, # ERA-Interim - 'dataset': self._dataset, - "expver": f"{self._expver}", - # They warn me against all, but it works well - "levelist": 'all', - "levtype": "ml", # Model levels - "param": "lnsp/q/z/t", # Necessary variables - "stream": "oper", - # date: Specify a single date as "2015-08-01" or a period as - # "2015-08-01/to/2015-08-31". - "date": datetime.datetime.strftime(corrected_DT, "%Y-%m-%d"), - # type: Use an (analysis) unless you have a particular reason to - # use fc (forecast). - "type": "an", - # time: With type=an, time can be any of - # "00:00:00/06:00:00/12:00:00/18:00:00". With type=fc, time can - # be any of "00:00:00/12:00:00", - "time": datetime.time.strftime(corrected_DT.time(), "%H:%M:%S"), - # step: With type=an, step is always "0". With type=fc, step can - # be any of "3/6/9/12". - "step": "0", - # grid: Only regular lat/lon grids are supported. - "grid": f'{lat_step}/{lon_step}', - "area": f'{lat_max}/{lon_min}/{lat_min}/{lon_max}', # area: N/W/S/E - "format": "netcdf", - "resol": "av", - "target": out, # target: the name of the output file. - }) - - - def _get_from_cds( - self, - lat_min, - lat_max, - lon_min, - lon_max, - acqTime, - outname - ) -> None: + server.retrieve( + { + 'class': self._classname, # ERA-Interim + 'dataset': self._dataset, + 'expver': f'{self._expver}', + # They warn me against all, but it works well + 'levelist': 'all', + 'levtype': 'ml', # Model levels + 'param': 'lnsp/q/z/t', # Necessary variables + 'stream': 'oper', + # date: Specify a single date as "2015-08-01" or a period as + # "2015-08-01/to/2015-08-31". + 'date': datetime.datetime.strftime(corrected_DT, '%Y-%m-%d'), + # type: Use an (analysis) unless you have a particular reason to + # use fc (forecast). + 'type': 'an', + # time: With type=an, time can be any of + # "00:00:00/06:00:00/12:00:00/18:00:00". With type=fc, time can + # be any of "00:00:00/12:00:00", + 'time': datetime.time.strftime(corrected_DT.time(), '%H:%M:%S'), + # step: With type=an, step is always "0". With type=fc, step can + # be any of "3/6/9/12". + 'step': '0', + # grid: Only regular lat/lon grids are supported. + 'grid': f'{lat_step}/{lon_step}', + 'area': f'{lat_max}/{lon_min}/{lat_min}/{lon_max}', # area: N/W/S/E + 'format': 'netcdf', + 'resol': 'av', + 'target': out, # target: the name of the output file. + } + ) + + def _get_from_cds(self, lat_min, lat_max, lon_min, lon_max, acqTime, outname) -> None: """Used for ERA5.""" import cdsapi + c = cdsapi.Client(verify=0) if self._model_level_type == 'pl': var = ['z', 'q', 't'] else: - var = "129/130/133/152" # 'lnsp', 'q', 'z', 't' + var = '129/130/133/152' # 'lnsp', 'q', 'z', 't' bbox = [lat_max, lon_min, lat_min, lon_max] @@ -204,36 +177,35 @@ def _get_from_cds( if not corrected_DT == acqTime: logger.warning('Rounded given datetime from %s to %s', acqTime, corrected_DT) - # I referenced https://confluence.ecmwf.int/display/CKB/How+to+download+ERA5 dataDict = { - "class": "ea", - "expver": "1", - "levelist": 'all', - "levtype": f"{self._model_level_type}", # 'ml' for model levels or 'pl' for pressure levels + 'class': 'ea', + 'expver': '1', + 'levelist': 'all', + 'levtype': f'{self._model_level_type}', # 'ml' for model levels or 'pl' for pressure levels 'param': var, - "stream": "oper", - "type": "an", - "date": "{}".format(corrected_DT.strftime('%Y-%m-%d')), - "time": "{}".format(datetime.time.strftime(corrected_DT.time(), '%H:%M')), + 'stream': 'oper', + 'type': 'an', + 'date': corrected_DT.strftime('%Y-%m-%d'), + 'time': datetime.time.strftime(corrected_DT.time(), '%H:%M'), # step: With type=an, step is always "0". With type=fc, step can # be any of "3/6/9/12". - "step": "0", - "area": bbox, - "grid": [0.25, .25], - "format": "netcdf"} + 'step': '0', + 'area': bbox, + 'grid': [0.25, 0.25], + 'format': 'netcdf', + } try: c.retrieve('reanalysis-era5-complete', dataDict, outname) except Exception: raise Exception - def _download_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step, time, out) -> None: """Used for HRES.""" from ecmwfapi import ECMWFService - server = ECMWFService("mars") + server = ECMWFService('mars') # round to the closest legal time corrected_DT = util.round_date(time, datetime.timedelta(hours=self._time_res)) @@ -241,32 +213,31 @@ def _download_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step logger.warning('Rounded given datetime from %s to %s', time, corrected_DT) if self._model_level_type == 'ml': - param = "129/130/133/152" + param = '129/130/133/152' else: - param = "129.128/130.128/133.128/152" + param = '129.128/130.128/133.128/152' server.execute( { 'class': self._classname, 'dataset': self._dataset, - 'expver': f"{self._expver}", - 'resol': "av", - 'stream': "oper", - 'type': "an", - 'levelist': "all", - 'levtype': f"{self._model_level_type}", + 'expver': f'{self._expver}', + 'resol': 'av', + 'stream': 'oper', + 'type': 'an', + 'levelist': 'all', + 'levtype': f'{self._model_level_type}', 'param': param, - 'date': datetime.datetime.strftime(corrected_DT, "%Y-%m-%d"), - 'time': "{}".format(datetime.time.strftime(corrected_DT.time(), '%H:%M')), - 'step': "0", - 'grid': f"{lon_step}/{lat_step}", - 'area': f"{lat_max}/{util.floorish(lon_min, 0.1)}/{util.floorish(lat_min, 0.1)}/{lon_max}", - 'format': "netcdf", + 'date': datetime.datetime.strftime(corrected_DT, '%Y-%m-%d'), + 'time': datetime.time.strftime(corrected_DT.time(), '%H:%M'), + 'step': '0', + 'grid': f'{lon_step}/{lat_step}', + 'area': f'{lat_max}/{util.floorish(lon_min, 0.1)}/{util.floorish(lat_min, 0.1)}/{lon_max}', + 'format': 'netcdf', }, - out + out, ) - def _load_pressure_level(self, filename, *args, **kwargs) -> None: with xr.open_dataset(filename) as block: # Pull the data @@ -307,8 +278,7 @@ def _load_pressure_level(self, filename, *args, **kwargs) -> None: # correct heights for latitude self._get_heights(self._lats, geo_hgt) - self._p = np.broadcast_to(levels[np.newaxis, np.newaxis, :], - self._zs.shape) + self._p = np.broadcast_to(levels[np.newaxis, np.newaxis, :], self._zs.shape) # Re-structure from (heights, lats, lons) to (lons, lats, heights) self._t = self._t.transpose(1, 2, 0) @@ -321,7 +291,6 @@ def _load_pressure_level(self, filename, *args, **kwargs) -> None: self._t = np.flip(self._t, axis=2) self._q = np.flip(self._q, axis=2) - def _makeDataCubes(self, fname, verbose=False): """ Create a cube of data representing temperature and relative humidity @@ -350,7 +319,6 @@ def _makeDataCubes(self, fname, verbose=False): ys = lats.copy() if z.size == 0: - raise RuntimeError('There is no data in z, ' - 'you may have a problem with your mask') + raise RuntimeError('There is no data in z, you may have a problem with your mask') return lats, lons, xs, ys, t, q, lnsp, z diff --git a/tools/RAiDER/models/era5.py b/tools/RAiDER/models/era5.py index 182a5f8f0..64df87521 100755 --- a/tools/RAiDER/models/era5.py +++ b/tools/RAiDER/models/era5.py @@ -20,11 +20,11 @@ def __init__(self) -> None: self._proj = CRS.from_epsg(4326) # Tuple of min/max years where data is available. - lag_time = 3 # months + lag_time = 3 # months end_date = datetime.datetime.today() - relativedelta(months=lag_time) self._valid_range = ( - datetime.datetime(1950, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - end_date.replace(tzinfo=datetime.timezone(offset=datetime.timedelta())) + datetime.datetime(1950, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), + end_date.replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), ) # Availability lag time in days @@ -33,7 +33,6 @@ def __init__(self) -> None: # Default, need to change to ml self.setLevelType('ml') - def _fetch(self, out) -> None: """Fetch a weather model from ECMWF.""" # bounding box plus a buffer @@ -43,7 +42,6 @@ def _fetch(self, out) -> None: # execute the search at ECMWF self._get_from_cds(lat_min, lat_max, lon_min, lon_max, time, out) - def load_weather(self, f=None, *args, **kwargs) -> None: """Load either pressure or model level data.""" f = self.files[0] if f is None else f diff --git a/tools/RAiDER/models/era5t.py b/tools/RAiDER/models/era5t.py index 42b602faa..630bef086 100644 --- a/tools/RAiDER/models/era5t.py +++ b/tools/RAiDER/models/era5t.py @@ -13,9 +13,11 @@ def __init__(self) -> None: self._dataset = 'era5t' self._Name = 'ERA-5T' - self._valid_range = (datetime.datetime(1950, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc)) # Tuple of min/max years where data is available. + self._valid_range = ( + datetime.datetime(1950, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), + datetime.datetime.now(datetime.timezone.utc), + ) # Tuple of min/max years where data is available. # Availability lag time in days; actually about 12 hours but unstable on ECMWF side - # https://confluence.ecmwf.int/display/CKB/ERA5%3A+data+documentation - # see data update frequency + # https://confluence.ecmwf.int/display/CKB/ERA5%3A+data+documentation + # see data update frequency self._lag_time = datetime.timedelta(days=1) diff --git a/tools/RAiDER/models/erai.py b/tools/RAiDER/models/erai.py index d120d2219..d47ebfc09 100755 --- a/tools/RAiDER/models/erai.py +++ b/tools/RAiDER/models/erai.py @@ -18,7 +18,7 @@ def __init__(self) -> None: # Tuple of min/max years where data is available. self._valid_range = ( datetime.datetime(1979, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime(2019, 8, 31).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())) + datetime.datetime(2019, 8, 31).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), ) self._lag_time = datetime.timedelta(days=30) # Availability lag time in days diff --git a/tools/RAiDER/models/generateGACOSVRT.py b/tools/RAiDER/models/generateGACOSVRT.py index 911f7b925..3cfcad233 100644 --- a/tools/RAiDER/models/generateGACOSVRT.py +++ b/tools/RAiDER/models/generateGACOSVRT.py @@ -6,12 +6,17 @@ def makeVRT(filename, dtype='Float32') -> None: """Use an RSC file to create a GDAL-compatible VRT file for opening GACOS weather model files.""" fields = readRSC(filename) - string = vrtStr(fields['XMAX'], fields['YMAX'], fields['X_FIRST'], fields['Y_FIRST'], fields['X_STEP'], fields['Y_STEP'], filename.replace('.rsc', ''), dtype=dtype) - writeStringToFile(string, filename.replace('.rsc', '').replace('.ztd', '') + '.vrt') - - -def writeStringToFile(string, filename): - """Write a string to a VRT file.""" + string = vrtStr( + fields['XMAX'], + fields['YMAX'], + fields['X_FIRST'], + fields['Y_FIRST'], + fields['X_STEP'], + fields['Y_STEP'], + filename.replace('.rsc', ''), + dtype=dtype, + ) + filename = filename.replace('.rsc', '').replace('.ztd', '') + '.vrt' with open(filename, 'w') as f: f.write(string) @@ -26,21 +31,21 @@ def readRSC(rscFilename): def vrtStr(xSize, ySize, lon1, lat1, lonStep, latStep, filename, dtype='Float32'): - string = f''' - EPSG:4326 - {lon1}, {lonStep}, 0.0000000000000000e+00, {lat1}, 0.0000000000000000e+00, {latStep} - - {filename} - - -''' - - return string + return ( + f'' + ' EPSG:4326' + f' {lon1}, {lonStep}, 0.0000000000000000e+00, {lat1}, 0.0000000000000000e+00, {latStep}' + f' ' + f' {filename}' + ' ' + '' + ) def convertAllFiles(dirLoc) -> None: """Convert all RSC files to VRT files contained in dirLoc.""" import glob + files = glob.glob('*.rsc') for f in files: makeVRT(f) @@ -48,6 +53,7 @@ def convertAllFiles(dirLoc) -> None: def main() -> None: import sys + if len(sys.argv) == 2: makeVRT(sys.argv[1]) elif len(sys.argv) == 3: diff --git a/tools/RAiDER/models/gmao.py b/tools/RAiDER/models/gmao.py index 8e22f79ea..f7130ba40 100755 --- a/tools/RAiDER/models/gmao.py +++ b/tools/RAiDER/models/gmao.py @@ -30,8 +30,10 @@ def __init__(self) -> None: self._dataset = 'gmao' # Tuple of min/max years where data is available. - self._valid_range = (datetime.datetime(2014, 2, 20).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc)) + self._valid_range = ( + datetime.datetime(2014, 2, 20).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), + datetime.datetime.now(datetime.timezone.utc), + ) self._lag_time = datetime.timedelta(hours=24.0) # Availability lag time in hours # model constants @@ -41,7 +43,6 @@ def __init__(self) -> None: self._time_res = TIME_RES[self._dataset.upper()] - # horizontal grid spacing self._lat_res = 0.25 self._lon_res = 0.3125 @@ -57,7 +58,6 @@ def __init__(self) -> None: # Projection self._proj = CRS.from_epsg(4326) - def _fetch(self, out) -> None: """Fetch weather model data from GMAO.""" acqTime = self._time @@ -85,24 +85,27 @@ def _fetch(self, out) -> None: session = pydap.cas.urs.setup_session('username', 'password', check_url=url) ds = pydap.client.open_url(url, session=session) - p = ds['pl'].array[ - time_ind, - ml_min:(ml_max + 1), - lat_min_ind:(lat_max_ind + 1), - lon_min_ind:(lon_max_ind + 1) - ].data[0] - t = ds['t'].array[ - time_ind, - ml_min:(ml_max + 1), - lat_min_ind:(lat_max_ind + 1), - lon_min_ind:(lon_max_ind + 1) - ].data[0] - h = ds['h'].array[ - time_ind, - ml_min:(ml_max + 1), - lat_min_ind:(lat_max_ind + 1), - lon_min_ind:(lon_max_ind + 1) - ].data[0] + p = ( + ds['pl'] + .array[ + time_ind, ml_min : (ml_max + 1), lat_min_ind : (lat_max_ind + 1), lon_min_ind : (lon_max_ind + 1) + ] + .data[0] + ) + t = ( + ds['t'] + .array[ + time_ind, ml_min : (ml_max + 1), lat_min_ind : (lat_max_ind + 1), lon_min_ind : (lon_max_ind + 1) + ] + .data[0] + ) + h = ( + ds['h'] + .array[ + time_ind, ml_min : (ml_max + 1), lat_min_ind : (lat_max_ind + 1), lon_min_ind : (lon_max_ind + 1) + ] + .data[0] + ) else: root = 'https://portal.nccs.nasa.gov/datashare/gmao/geos-fp/das/Y{}/M{:02d}/D{:02d}' @@ -120,29 +123,22 @@ def _fetch(self, out) -> None: logger.warning('Weather model already exists, skipping download') with h5py.File(f, 'r') as ds: - q = ds['QV'][0, :, lat_min_ind:(lat_max_ind + 1), lon_min_ind:(lon_max_ind + 1)] - p = ds['PL'][0, :, lat_min_ind:(lat_max_ind + 1), lon_min_ind:(lon_max_ind + 1)] - t = ds['T'][0, :, lat_min_ind:(lat_max_ind + 1), lon_min_ind:(lon_max_ind + 1)] - h = ds['H'][0, :, lat_min_ind:(lat_max_ind + 1), lon_min_ind:(lon_max_ind + 1)] + q = ds['QV'][0, :, lat_min_ind : (lat_max_ind + 1), lon_min_ind : (lon_max_ind + 1)] + p = ds['PL'][0, :, lat_min_ind : (lat_max_ind + 1), lon_min_ind : (lon_max_ind + 1)] + t = ds['T'][0, :, lat_min_ind : (lat_max_ind + 1), lon_min_ind : (lon_max_ind + 1)] + h = ds['H'][0, :, lat_min_ind : (lat_max_ind + 1), lon_min_ind : (lon_max_ind + 1)] os.remove(f) - lats = np.arange( - (-90 + lat_min_ind * self._lat_res), - (-90 + (lat_max_ind + 1) * self._lat_res), - self._lat_res - ) + lats = np.arange((-90 + lat_min_ind * self._lat_res), (-90 + (lat_max_ind + 1) * self._lat_res), self._lat_res) lons = np.arange( - (-180 + lon_min_ind * self._lon_res), - (-180 + (lon_max_ind + 1) * self._lon_res), - self._lon_res + (-180 + lon_min_ind * self._lon_res), (-180 + (lon_max_ind + 1) * self._lon_res), self._lon_res ) try: # Note that lat/lon gets written twice for GMAO because they are the same as y/x writeWeatherVarsXarray(lats, lons, h, q, p, t, dt, crs, outName=None, NoDataValue=None, chunk=(1, 91, 144)) except Exception: - logger.exception("Unable to save weathermodel to file") - + logger.exception('Unable to save weathermodel to file') def load_weather(self, f=None) -> None: """ @@ -154,11 +150,11 @@ def load_weather(self, f=None) -> None: f = self.files[0] if f is None else f self._load_model_level(f) - def _load_model_level(self, filename) -> None: """Get the variables from the GMAO link using OpenDAP.""" # adding the import here should become absolute when transition to netcdf from netCDF4 import Dataset + with Dataset(filename, mode='r') as f: lons = np.array(f.variables['x'][:]) lats = np.array(f.variables['y'][:]) @@ -168,7 +164,7 @@ def _load_model_level(self, filename) -> None: t = np.array(f.variables['T'][:]) # restructure the 1-D lat/lon in regular 2D grid - _lons, _lats= np.meshgrid(lons, lats) + _lons, _lats = np.meshgrid(lons, lats) # Re-structure everything from (heights, lats, lons) to (lons, lats, heights) p = np.transpose(p) diff --git a/tools/RAiDER/models/hres.py b/tools/RAiDER/models/hres.py index f8f1807e7..e065f8ecf 100755 --- a/tools/RAiDER/models/hres.py +++ b/tools/RAiDER/models/hres.py @@ -20,16 +20,15 @@ def __init__(self, level_type='ml') -> None: WeatherModel.__init__(self) # model constants - self._k1 = 0.776 # [K/Pa] - self._k2 = 0.233 # [K/Pa] + self._k1 = 0.776 # [K/Pa] + self._k2 = 0.233 # [K/Pa] self._k3 = 3.75e3 # [K^2/Pa] - # 9 km horizontal grid spacing. This is only used for extending the download-buffer, i.e. not in subsequent processing. - self._lon_res = 9. / 111 # 0.08108115 - self._lat_res = 9. / 111 # 0.08108115 - self._x_res = 9. / 111 # 0.08108115 - self._y_res = 9. / 111 # 0.08108115 + self._lon_res = 9.0 / 111 # 0.08108115 + self._lat_res = 9.0 / 111 # 0.08108115 + self._x_res = 9.0 / 111 # 0.08108115 + self._y_res = 9.0 / 111 # 0.08108115 self._humidityType = 'q' # Default, pressure levels are 'pl' @@ -41,8 +40,10 @@ def __init__(self, level_type='ml') -> None: self._time_res = TIME_RES[self._dataset.upper()] # Tuple of min/max years where data is available. - self._valid_range = (datetime.datetime(1983, 4, 20).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc)) + self._valid_range = ( + datetime.datetime(1983, 4, 20).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), + datetime.datetime.now(datetime.timezone.utc), + ) # Availability lag time in days self._lag_time = datetime.timedelta(hours=6) @@ -66,19 +67,19 @@ def load_weather(self, f=None) -> None: f = self.files[0] if f is None else f if self._model_level_type == 'ml': - if (self._time < datetime.datetime(2013, 6, 26, 0, 0, 0)): + if self._time < datetime.datetime(2013, 6, 26, 0, 0, 0): self.update_a_b() self._load_model_level(f) elif self._model_level_type == 'pl': self._load_pressure_levels(f) - def _fetch(self,out) -> None: + def _fetch(self, out) -> None: """Fetch a weather model from ECMWF.""" # bounding box plus a buffer lat_min, lat_max, lon_min, lon_max = self._ll_bounds time = self._time - if (time < datetime.datetime(2013, 6, 26, 0, 0, 0)): + if time < datetime.datetime(2013, 6, 26, 0, 0, 0): self.update_a_b() # execute the search at ECMWF diff --git a/tools/RAiDER/models/hrrr.py b/tools/RAiDER/models/hrrr.py index ecf8d2554..50bc526ed 100644 --- a/tools/RAiDER/models/hrrr.py +++ b/tools/RAiDER/models/hrrr.py @@ -17,20 +17,24 @@ HRRR_CONUS_COVERAGE_POLYGON = Polygon(((-125, 21), (-133, 49), (-60, 49), (-72, 21))) HRRR_AK_COVERAGE_POLYGON = Polygon(((195, 40), (157, 55), (175, 70), (260, 77), (232, 52))) -HRRR_AK_PROJ = CRS.from_string('+proj=stere +ellps=sphere +a=6371229.0 +b=6371229.0 +lat_0=90 +lon_0=225.0 ' - '+x_0=0.0 +y_0=0.0 +lat_ts=60.0 +no_defs +type=crs') +HRRR_AK_PROJ = CRS.from_string( + '+proj=stere +ellps=sphere +a=6371229.0 +b=6371229.0 +lat_0=90 +lon_0=225.0 ' + '+x_0=0.0 +y_0=0.0 +lat_ts=60.0 +no_defs +type=crs' +) # Source: https://eric.clst.org/tech/usgeojson/ AK_GEO = gpd.read_file(Path(__file__).parent / 'data' / 'alaska.geojson.zip').geometry.unary_union def check_hrrr_dataset_availability(dt: datetime) -> bool: """Note a file could still be missing within the models valid range.""" - H = Herbie(dt, - model='hrrr', - product='nat', - fxx=0) - avail = (H.grib_source is not None) - return avail + herbie = Herbie( + dt, + model='hrrr', + product='nat', + fxx=0, + ) + return herbie.grib_source is not None + def download_hrrr_file(ll_bounds, DATE, out, model='hrrr', product='nat', fxx=0, verbose=False) -> None: """ @@ -48,7 +52,7 @@ def download_hrrr_file(ll_bounds, DATE, out, model='hrrr', product='nat', fxx=0, Returns: None, writes data to a netcdf file """ - H = Herbie( + herbie = Herbie( DATE.strftime('%Y-%m-%d %H:%M'), model=model, product=product, @@ -58,13 +62,12 @@ def download_hrrr_file(ll_bounds, DATE, out, model='hrrr', product='nat', fxx=0, save_dir=Path(os.path.dirname(out)), ) - # Iterate through the list of datasets try: - ds_list = H.xarray(":(SPFH|PRES|TMP|HGT):", verbose=verbose) - except ValueError as E: - logger.error (E) - raise ValueError + ds_list = herbie.xarray(':(SPFH|PRES|TMP|HGT):', verbose=verbose) + except ValueError as e: + logger.error(e) + raise ds_out = None # Note order coord names are request for `test_HRRR_ztd` matters @@ -94,31 +97,28 @@ def download_hrrr_file(ll_bounds, DATE, out, model='hrrr', product='nat', fxx=0, ds_out = ds_out.rename({'gh': 'z', coord: 'levels'}) # projection information - ds_out["proj"] = 0 + ds_out['proj'] = 0 for k, v in CRS.from_user_input(ds_out.herbie.crs).to_cf().items(): ds_out.proj.attrs[k] = v for var in ds_out.data_vars: ds_out[var].attrs['grid_mapping'] = 'proj' - # pull the grid information proj = CRS.from_cf(ds_out['proj'].attrs) t = Transformer.from_crs(4326, proj, always_xy=True) xl, yl = t.transform(ds_out['longitude'].values, ds_out['latitude'].values) W, E, S, N = np.nanmin(xl), np.nanmax(xl), np.nanmin(yl), np.nanmax(yl) - grid_x = 3000 # meters - grid_y = 3000 # meters - xs = np.arange(W, E+grid_x/2, grid_x) - ys = np.arange(S, N+grid_y/2, grid_y) + grid_x = 3000 # meters + grid_y = 3000 # meters + xs = np.arange(W, E + grid_x / 2, grid_x) + ys = np.arange(S, N + grid_y / 2, grid_y) ds_out['x'] = xs ds_out['y'] = ys ds_sub = ds_out.isel(x=slice(x_min, x_max), y=slice(y_min, y_max)) ds_sub.to_netcdf(out, engine='netcdf4') - return - def get_bounds_indices(SNWE, lats, lons): """Convert SNWE lat/lon bounds to index bounds.""" @@ -130,8 +130,8 @@ def get_bounds_indices(SNWE, lats, lons): m1 = (S <= lats) & (N >= lats) & (W <= lons) & (E >= lons) else: raise ValueError( - 'Longitude is either flipped or you are crossing the international date line;' + - 'if the latter please give me longitudes from 0-360' + 'Longitude is either flipped or you are crossing the international date line;' + + 'if the latter please give me longitudes from 0-360' ) if np.sum(m1) == 0: @@ -177,10 +177,8 @@ def load_weather_hrrr(filename): lons[lons > 180] -= 360 # data cube format should be lats,lons,heights - _xs = np.broadcast_to(xArr[np.newaxis, :, np.newaxis], - geo_hgt.shape) - _ys = np.broadcast_to(yArr[:, np.newaxis, np.newaxis], - geo_hgt.shape) + _xs = np.broadcast_to(xArr[np.newaxis, :, np.newaxis], geo_hgt.shape) + _ys = np.broadcast_to(yArr[:, np.newaxis, np.newaxis], geo_hgt.shape) return _xs, _ys, lons, lats, qs, temps, pres, geo_hgt, proj @@ -200,8 +198,8 @@ def __init__(self) -> None: # Tuple of min/max years where data is available. self._valid_range = ( - datetime.datetime(2016, 7, 15).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc) + datetime.datetime(2016, 7, 15).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), + datetime.datetime.now(datetime.timezone.utc), ) self._lag_time = datetime.timedelta(hours=3) # Availability lag time in days @@ -211,10 +209,10 @@ def __init__(self) -> None: self._k3 = 3.75e3 # [K^2/Pa] # 3 km horizontal grid spacing - self._lat_res = 3. / 111 - self._lon_res = 3. / 111 - self._x_res = 3. - self._y_res = 3. + self._lat_res = 3.0 / 111 + self._lon_res = 3.0 / 111 + self._x_res = 3.0 + self._y_res = 3.0 self._Nproc = 1 self._Name = 'HRRR' @@ -239,23 +237,22 @@ def __init__(self) -> None: x0 = 0 y0 = 0 earth_radius = 6371229 - self._proj = CRS(f'+proj=lcc +lat_1={lat1} +lat_2={lat2} +lat_0={lat0} '\ - f'+lon_0={lon0} +x_0={x0} +y_0={y0} +a={earth_radius} '\ - f'+b={earth_radius} +units=m +no_defs') + self._proj = CRS( + f'+proj=lcc +lat_1={lat1} +lat_2={lat2} +lat_0={lat0} ' + f'+lon_0={lon0} +x_0={x0} +y_0={y0} +a={earth_radius} ' + f'+b={earth_radius} +units=m +no_defs' + ) self._valid_bounds = HRRR_CONUS_COVERAGE_POLYGON self.setLevelType('nat') - def __model_levels__(self): - self._levels = 50 + self._levels = 50 self._zlevels = np.flipud(LEVELS_50_HEIGHTS) - def __pressure_levels__(self): raise NotImplementedError('Pressure levels do not go high enough for HRRR.') - - def _fetch(self, out) -> None: + def _fetch(self, out) -> None: """Fetch weather model data from HRRR.""" self._files = out corrected_DT = round_date(self._time, datetime.timedelta(hours=self._time_res)) @@ -269,7 +266,6 @@ def _fetch(self, out) -> None: download_hrrr_file(bounds, corrected_DT, out, 'hrrr', self._model_level_type) - def load_weather(self, f=None, *args, **kwargs) -> None: """ Load a weather model into a python weatherModel object, from self.files if no @@ -278,7 +274,6 @@ def load_weather(self, f=None, *args, **kwargs) -> None: if f is None: f = self.files[0] if isinstance(self.files, list) else self.files - _xs, _ys, _lons, _lats, qs, temps, pres, geo_hgt, proj = load_weather_hrrr(f) # convert geopotential height to geometric height @@ -293,7 +288,6 @@ def load_weather(self, f=None, *args, **kwargs) -> None: self._lons = _lons self._proj = proj - def checkValidBounds(self: WeatherModel, ll_bounds: np.ndarray): """ Checks whether the given bounding box is valid for the HRRR or HRRRAK @@ -318,7 +312,7 @@ def checkValidBounds(self: WeatherModel, ll_bounds: np.ndarray): Mod = HRRRAK() # valid bounds are in 0->360 to account for dateline crossing W, E = np.mod([W, E], 360) - aoi = box(W, S, E, N) + aoi = box(W, S, E, N) if Mod._valid_bounds.contains(aoi): pass elif aoi.intersects(Mod._valid_bounds): @@ -342,10 +336,10 @@ def __init__(self) -> None: self._k3 = 3.75e3 # [K^2/Pa] # 3 km horizontal grid spacing - self._lat_res = 3. / 111 - self._lon_res = 3. / 111 - self._x_res = 3. - self._y_res = 3. + self._lat_res = 3.0 / 111 + self._lon_res = 3.0 / 111 + self._x_res = 3.0 + self._y_res = 3.0 self._Nproc = 1 self._Npl = 0 @@ -354,11 +348,11 @@ def __init__(self) -> None: self._classname = 'hrrrak' self._dataset = 'hrrrak' - self._Name = "HRRR-AK" + self._Name = 'HRRR-AK' self._time_res = TIME_RES['HRRR-AK'] self._valid_range = ( - datetime.datetime(2018, 7, 13).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc) + datetime.datetime(2018, 7, 13).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), + datetime.datetime.now(datetime.timezone.utc), ) self._lag_time = datetime.timedelta(hours=3) self._valid_bounds = HRRR_AK_COVERAGE_POLYGON @@ -367,15 +361,14 @@ def __init__(self) -> None: self._proj = HRRR_AK_PROJ self.setLevelType('nat') - def __model_levels__(self): - self._levels = 50 + self._levels = 50 self._zlevels = np.flipud(LEVELS_50_HEIGHTS) - def __pressure_levels__(self): - raise NotImplementedError('hrrr.py: Revisit whether or not pressure levels from HRRR can be used for delay calculations; they do not go high enough compared to native model levels.') - + raise NotImplementedError( + 'hrrr.py: Revisit whether or not pressure levels from HRRR can be used for delay calculations; they do not go high enough compared to native model levels.' + ) def _fetch(self, out) -> None: bounds = self._ll_bounds.copy() @@ -387,7 +380,6 @@ def _fetch(self, out) -> None: download_hrrr_file(bounds, corrected_DT, out, 'hrrrak', self._model_level_type) - def load_weather(self, f=None, *args, **kwargs) -> None: if f is None: f = self.files[0] if isinstance(self.files, list) else self.files diff --git a/tools/RAiDER/models/merra2.py b/tools/RAiDER/models/merra2.py index ffda1cc29..fc251d42f 100755 --- a/tools/RAiDER/models/merra2.py +++ b/tools/RAiDER/models/merra2.py @@ -17,7 +17,7 @@ # Path to Netrc file, can be controlled by env var # Useful for containers - similar to CDSAPI_RC -EARTHDATA_RC = os.environ.get("EARTHDATA_RC", None) +EARTHDATA_RC = os.environ.get('EARTHDATA_RC', None) def Model(): @@ -26,8 +26,8 @@ def Model(): class MERRA2(WeatherModel): def __init__(self) -> None: - import calendar + # initialize a weather model WeatherModel.__init__(self) @@ -41,12 +41,14 @@ def __init__(self) -> None: utcnow = datetime.datetime.now(datetime.timezone.utc) enddate = datetime.datetime(utcnow.year, utcnow.month, 15) - datetime.timedelta(days=60) enddate = datetime.datetime(enddate.year, enddate.month, calendar.monthrange(enddate.year, enddate.month)[1]) - self._valid_range = (datetime.datetime(1980, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc)) + self._valid_range = ( + datetime.datetime(1980, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), + datetime.datetime.now(datetime.timezone.utc), + ) lag_time = utcnow - enddate.replace(tzinfo=datetime.timezone(offset=datetime.timedelta())) self._lag_time = datetime.timedelta(days=lag_time.days) # Availability lag time in days self._time_res = 1 - + # model constants self._k1 = 0.776 # [K/Pa] self._k2 = 0.233 # [K/Pa] @@ -68,8 +70,8 @@ def __init__(self) -> None: def _fetch(self, out) -> None: """Fetch weather model data from GMAO: note we only extract the lat/lon bounds for this weather model; fetching data is not needed here as we don't actually download any data using OpenDAP.""" - time = self._time - + time = self._time + # check whether the file already exists if os.path.exists(out): return @@ -80,15 +82,9 @@ def _fetch(self, out) -> None: lon_min_ind = int((self._ll_bounds[2] - (-180.0)) / self._lon_res) lon_max_ind = int((self._ll_bounds[3] - (-180.0)) / self._lon_res) - lats = np.arange( - (-90 + lat_min_ind * self._lat_res), - (-90 + (lat_max_ind + 1) * self._lat_res), - self._lat_res - ) + lats = np.arange((-90 + lat_min_ind * self._lat_res), (-90 + (lat_max_ind + 1) * self._lat_res), self._lat_res) lons = np.arange( - (-180 + lon_min_ind * self._lon_res), - (-180 + (lon_max_ind + 1) * self._lon_res), - self._lon_res + (-180 + lon_min_ind * self._lon_res), (-180 + (lon_max_ind + 1) * self._lon_res), self._lon_res ) lon, lat = np.meshgrid(lons, lats) @@ -106,24 +102,32 @@ def _fetch(self, out) -> None: earthdata_usr, earthdata_pwd = read_EarthData_loginInfo(EARTHDATA_RC) # open the dataset and pull the data - url = 'https://goldsmr5.gesdisc.eosdis.nasa.gov/opendap/MERRA2/M2T3NVASM.5.12.4/' + time.strftime('%Y/%m') + '/MERRA2_' + str(url_sub) + '.tavg3_3d_asm_Nv.' + time.strftime('%Y%m%d') + '.nc4' + url = ( + 'https://goldsmr5.gesdisc.eosdis.nasa.gov/opendap/MERRA2/M2T3NVASM.5.12.4/' + + time.strftime('%Y/%m') + + '/MERRA2_' + + str(url_sub) + + '.tavg3_3d_asm_Nv.' + + time.strftime('%Y%m%d') + + '.nc4' + ) session = pydap.cas.urs.setup_session(earthdata_usr, earthdata_pwd, check_url=url) stream = pydap.client.open_url(url, session=session) - q = stream['QV'][0,:,lat_min_ind:lat_max_ind + 1, lon_min_ind:lon_max_ind + 1].data.squeeze() - p = stream['PL'][0,:,lat_min_ind:lat_max_ind + 1, lon_min_ind:lon_max_ind + 1].data.squeeze() - t = stream['T'][0,:,lat_min_ind:lat_max_ind + 1, lon_min_ind:lon_max_ind + 1].data.squeeze() - h = stream['H'][0,:,lat_min_ind:lat_max_ind + 1, lon_min_ind:lon_max_ind + 1].data.squeeze() + q = stream['QV'][0, :, lat_min_ind : lat_max_ind + 1, lon_min_ind : lon_max_ind + 1].data.squeeze() + p = stream['PL'][0, :, lat_min_ind : lat_max_ind + 1, lon_min_ind : lon_max_ind + 1].data.squeeze() + t = stream['T'][0, :, lat_min_ind : lat_max_ind + 1, lon_min_ind : lon_max_ind + 1].data.squeeze() + h = stream['H'][0, :, lat_min_ind : lat_max_ind + 1, lon_min_ind : lon_max_ind + 1].data.squeeze() try: writeWeatherVarsXarray(lat, lon, h, q, p, t, time, self._proj, outName=out) except Exception as e: logger.debug(e) - logger.exception("MERRA-2: Unable to save weathermodel to file") + logger.exception('MERRA-2: Unable to save weathermodel to file') raise RuntimeError(f'MERRA-2 failed with the following error: {e}') - def load_weather(self, f=None, *args, **kwargs) -> None: + def load_weather(self, f=None, *args, **kwargs) -> None: """ Consistent class method to be implemented across all weather model types. As a result of calling this method, all of the variables (x, y, z, p, q, diff --git a/tools/RAiDER/models/model_levels.py b/tools/RAiDER/models/model_levels.py index 549c5d2ae..56891580d 100644 --- a/tools/RAiDER/models/model_levels.py +++ b/tools/RAiDER/models/model_levels.py @@ -506,7 +506,7 @@ -500, ] -## HRRR Model Levels +# HRRR Model Levels # Computed according to: H = a + b * Z where: # H is the resulting levels in geometric height # a is the Surface geopotential height (in meters) @@ -514,16 +514,18 @@ # averaged in space over CONUS # b is the native (sigma) model levels (https://rapidrefresh.noaa.gov/faq/HRRR.faq.html) # Z is the spatial average geopotential height of the sigma level (in meters) -LEVELS_50_HEIGHTS = [2.61580385e+04, 2.48712879e+04, 2.36910518e+04, 2.25524744e+04, - 2.13986900e+04, 2.02464207e+04, 1.90883153e+04, 1.79427740e+04, - 1.68476065e+04, 1.57399654e+04, 1.45826790e+04, 1.33886515e+04, - 1.22171878e+04, 1.11019360e+04, 1.00395775e+04, 9.01965365e+03, - 8.03486128e+03, 7.09323111e+03, 6.27822334e+03, 5.57101666e+03, - 4.96120000e+03, 4.42159162e+03, 3.94118518e+03, 3.51064883e+03, - 3.12371808e+03, 2.77490670e+03, 2.45941860e+03, 2.17290722e+03, - 1.90394551e+03, 1.66716448e+03, 1.44127808e+03, 1.22697117e+03, - 1.02507126e+03, 8.38877887e+02, 6.74297597e+02, 5.34810131e+02, - 4.18916771e+02, 3.23291544e+02, 2.44985788e+02, 1.81492083e+02, - 1.34383211e+02, 1.02007390e+02, 7.70762881e+01, 5.77739913e+01, - 4.31591299e+01, 3.26389095e+01, 2.52657431e+01, 2.02104423e+01, - 1.66520787e+01, 1.39366382e+01, 0, -10, -20, -50, -100, -200, -500] \ No newline at end of file +LEVELS_50_HEIGHTS = [ + 2.61580385e+04, 2.48712879e+04, 2.36910518e+04, 2.25524744e+04, + 2.13986900e+04, 2.02464207e+04, 1.90883153e+04, 1.79427740e+04, + 1.68476065e+04, 1.57399654e+04, 1.45826790e+04, 1.33886515e+04, + 1.22171878e+04, 1.11019360e+04, 1.00395775e+04, 9.01965365e+03, + 8.03486128e+03, 7.09323111e+03, 6.27822334e+03, 5.57101666e+03, + 4.96120000e+03, 4.42159162e+03, 3.94118518e+03, 3.51064883e+03, + 3.12371808e+03, 2.77490670e+03, 2.45941860e+03, 2.17290722e+03, + 1.90394551e+03, 1.66716448e+03, 1.44127808e+03, 1.22697117e+03, + 1.02507126e+03, 8.38877887e+02, 6.74297597e+02, 5.34810131e+02, + 4.18916771e+02, 3.23291544e+02, 2.44985788e+02, 1.81492083e+02, + 1.34383211e+02, 1.02007390e+02, 7.70762881e+01, 5.77739913e+01, + 4.31591299e+01, 3.26389095e+01, 2.52657431e+01, 2.02104423e+01, + 1.66520787e+01, 1.39366382e+01, 0, -10, -20, -50, -100, -200, -500 +] diff --git a/tools/RAiDER/models/ncmr.py b/tools/RAiDER/models/ncmr.py index 5d3b92393..b490f68f6 100755 --- a/tools/RAiDER/models/ncmr.py +++ b/tools/RAiDER/models/ncmr.py @@ -2,6 +2,7 @@ Created on Wed Sep 9 10:26:44 2020 @author: prashant Modified by Yang Lei, GPS/Caltech """ + import datetime import os import urllib.request @@ -28,30 +29,32 @@ def __init__(self) -> None: # initialize a weather model WeatherModel.__init__(self) - self._humidityType = 'q' # q for specific humidity and rh for relative humidity - self._model_level_type = 'ml' # Default, pressure levels are 'pl', and model levels are "ml" - self._classname = 'ncmr' # name of the custom weather model - self._dataset = 'ncmr' # same name as above - self._Name = 'NCMR' # name of the new weather model (in Capital) + self._humidityType = 'q' # q for specific humidity and rh for relative humidity + self._model_level_type = 'ml' # Default, pressure levels are 'pl', and model levels are "ml" + self._classname = 'ncmr' # name of the custom weather model + self._dataset = 'ncmr' # same name as above + self._Name = 'NCMR' # name of the new weather model (in Capital) self._time_res = TIME_RES[self._dataset.upper()] # Tuple of min/max years where data is available. - self._valid_range = (datetime.datetime(2015, 12, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc)) + self._valid_range = ( + datetime.datetime(2015, 12, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), + datetime.datetime.now(datetime.timezone.utc), + ) # Availability lag time in days/hours self._lag_time = datetime.timedelta(hours=6) # model constants - self._k1 = 0.776 # [K/Pa] - self._k2 = 0.233 # [K/Pa] + self._k1 = 0.776 # [K/Pa] + self._k2 = 0.233 # [K/Pa] self._k3 = 3.75e3 # [K^2/Pa] # horizontal grid spacing - self._lon_res = .17578125 # grid spacing in longitude - self._lat_res = .11718750 # grid spacing in latitude + self._lon_res = 0.17578125 # grid spacing in longitude + self._lat_res = 0.11718750 # grid spacing in latitude - self._x_res = .17578125 # same as longitude - self._y_res = .11718750 # same as latitude + self._x_res = 0.17578125 # same as longitude + self._y_res = 0.11718750 # same as latitude self._zlevels = np.flipud(LEVELS_137_HEIGHTS) @@ -63,14 +66,14 @@ def __init__(self) -> None: def _fetch(self, out) -> None: """ Fetch weather model data from NCMR: note we only extract the lat/lon bounds for this weather model; - fetching data is not needed here as we don't actually download data , data exist in same system. + fetching data is not needed here as we don't actually download data, data exist in same system. """ time = self._time # Auxillary function: - ''' + """ download data of the NCMR model and save it in desired location - ''' + """ self._files = self._download_ncmr_file(out, time, self._ll_bounds) def load_weather(self, f=None, *args, **kwargs) -> None: @@ -103,17 +106,17 @@ def _download_ncmr_file(self, out, date_time, bounding_box) -> None: ######################################################################################################################## ############# For debugging: use pre-downloaded files; Remove/comment out it when actually downloading NCMR data from a weblink ############# -# filepath = os.path.dirname(out) + '/NCUM_ana_mdllev_20180701_00z.nc' + # filepath = os.path.dirname(out) + '/NCUM_ana_mdllev_20180701_00z.nc' ######################################################################################################################## # calculate the array indices for slicing the GMAO variable arrays lat_min_ind = int((self._bounds[0] - (-89.94141)) / self._lat_res) lat_max_ind = int((self._bounds[1] - (-89.94141)) / self._lat_res) - if (self._bounds[2] < 0.0): + if self._bounds[2] < 0.0: lon_min_ind = int((self._bounds[2] + 360.0 - (0.087890625)) / self._lon_res) else: lon_min_ind = int((self._bounds[2] - (0.087890625)) / self._lon_res) - if (self._bounds[3] < 0.0): + if self._bounds[3] < 0.0: lon_max_ind = int((self._bounds[3] + 360.0 - (0.087890625)) / self._lon_res) else: lon_max_ind = int((self._bounds[3] - (0.087890625)) / self._lon_res) @@ -122,41 +125,63 @@ def _download_ncmr_file(self, out, date_time, bounding_box) -> None: ml_max = 70 with Dataset(filepath, 'r', maskandscale=True) as f: - lats = f.variables['latitude'][lat_min_ind:(lat_max_ind + 1)].copy() - if (self._bounds[2] * self._bounds[3] < 0): + lats = f.variables['latitude'][lat_min_ind : (lat_max_ind + 1)].copy() + if self._bounds[2] * self._bounds[3] < 0: lons1 = f.variables['longitude'][lon_min_ind:].copy() - lons2 = f.variables['longitude'][0:(lon_max_ind + 1)].copy() + lons2 = f.variables['longitude'][0 : (lon_max_ind + 1)].copy() lons = np.append(lons1, lons2) else: - lons = f.variables['longitude'][lon_min_ind:(lon_max_ind + 1)].copy() - if (self._bounds[2] * self._bounds[3] < 0): - t1 = f.variables['air_temperature'][ml_min:(ml_max + 1), lat_min_ind:(lat_max_ind + 1), lon_min_ind:].copy() - t2 = f.variables['air_temperature'][ml_min:(ml_max + 1), lat_min_ind:(lat_max_ind + 1), 0:(lon_max_ind + 1)].copy() + lons = f.variables['longitude'][lon_min_ind : (lon_max_ind + 1)].copy() + if self._bounds[2] * self._bounds[3] < 0: + t1 = f.variables['air_temperature'][ + ml_min : (ml_max + 1), lat_min_ind : (lat_max_ind + 1), lon_min_ind: + ].copy() + t2 = f.variables['air_temperature'][ + ml_min : (ml_max + 1), lat_min_ind : (lat_max_ind + 1), 0 : (lon_max_ind + 1) + ].copy() t = np.append(t1, t2, axis=2) else: - t = f.variables['air_temperature'][ml_min:(ml_max + 1), lat_min_ind:(lat_max_ind + 1), lon_min_ind:(lon_max_ind + 1)].copy() + t = f.variables['air_temperature'][ + ml_min : (ml_max + 1), lat_min_ind : (lat_max_ind + 1), lon_min_ind : (lon_max_ind + 1) + ].copy() # Skipping first pressure levels (below 20 meter) - if (self._bounds[2] * self._bounds[3] < 0): - q1 = f.variables['specific_humidity'][(ml_min + 1):(ml_max + 1), lat_min_ind:(lat_max_ind + 1), lon_min_ind:].copy() - q2 = f.variables['specific_humidity'][(ml_min + 1):(ml_max + 1), lat_min_ind:(lat_max_ind + 1), 0:(lon_max_ind + 1)].copy() + if self._bounds[2] * self._bounds[3] < 0: + q1 = f.variables['specific_humidity'][ + (ml_min + 1) : (ml_max + 1), lat_min_ind : (lat_max_ind + 1), lon_min_ind: + ].copy() + q2 = f.variables['specific_humidity'][ + (ml_min + 1) : (ml_max + 1), lat_min_ind : (lat_max_ind + 1), 0 : (lon_max_ind + 1) + ].copy() q = np.append(q1, q2, axis=2) else: - q = f.variables['specific_humidity'][(ml_min + 1):(ml_max + 1), lat_min_ind:(lat_max_ind + 1), lon_min_ind:(lon_max_ind + 1)].copy() - if (self._bounds[2] * self._bounds[3] < 0): - p1 = f.variables['air_pressure'][(ml_min + 1):(ml_max + 1), lat_min_ind:(lat_max_ind + 1), lon_min_ind:].copy() - p2 = f.variables['air_pressure'][(ml_min + 1):(ml_max + 1), lat_min_ind:(lat_max_ind + 1), 0:(lon_max_ind + 1)].copy() + q = f.variables['specific_humidity'][ + (ml_min + 1) : (ml_max + 1), lat_min_ind : (lat_max_ind + 1), lon_min_ind : (lon_max_ind + 1) + ].copy() + if self._bounds[2] * self._bounds[3] < 0: + p1 = f.variables['air_pressure'][ + (ml_min + 1) : (ml_max + 1), lat_min_ind : (lat_max_ind + 1), lon_min_ind: + ].copy() + p2 = f.variables['air_pressure'][ + (ml_min + 1) : (ml_max + 1), lat_min_ind : (lat_max_ind + 1), 0 : (lon_max_ind + 1) + ].copy() p = np.append(p1, p2, axis=2) else: - p = f.variables['air_pressure'][(ml_min + 1):(ml_max + 1), lat_min_ind:(lat_max_ind + 1), lon_min_ind:(lon_max_ind + 1)].copy() - - level_hgt = f.variables['level_height'][(ml_min + 1):(ml_max + 1)].copy() - if (self._bounds[2] * self._bounds[3] < 0): - surface_alt1 = f.variables['surface_altitude'][lat_min_ind:(lat_max_ind + 1), lon_min_ind:].copy() - surface_alt2 = f.variables['surface_altitude'][lat_min_ind:(lat_max_ind + 1), 0:(lon_max_ind + 1)].copy() + p = f.variables['air_pressure'][ + (ml_min + 1) : (ml_max + 1), lat_min_ind : (lat_max_ind + 1), lon_min_ind : (lon_max_ind + 1) + ].copy() + + level_hgt = f.variables['level_height'][(ml_min + 1) : (ml_max + 1)].copy() + if self._bounds[2] * self._bounds[3] < 0: + surface_alt1 = f.variables['surface_altitude'][lat_min_ind : (lat_max_ind + 1), lon_min_ind:].copy() + surface_alt2 = f.variables['surface_altitude'][ + lat_min_ind : (lat_max_ind + 1), 0 : (lon_max_ind + 1) + ].copy() surface_alt = np.append(surface_alt1, surface_alt2, axis=1) else: - surface_alt = f.variables['surface_altitude'][lat_min_ind:(lat_max_ind + 1), lon_min_ind:(lon_max_ind + 1)].copy() + surface_alt = f.variables['surface_altitude'][ + lat_min_ind : (lat_max_ind + 1), lon_min_ind : (lon_max_ind + 1) + ].copy() hgt = np.zeros([len(level_hgt), len(surface_alt[:, 1]), len(surface_alt[1, :])]) for i in range(len(level_hgt)): @@ -171,7 +196,7 @@ def _download_ncmr_file(self, out, date_time, bounding_box) -> None: try: writeWeatherVarsXarray(lats, lons, hgt, q, p, t, self._time, self._proj, outName=out) except Exception: - logger.exception("Unable to save weathermodel to file") + logger.exception('Unable to save weathermodel to file') def _makeDataCubes(self, filename) -> None: """Get the variables from the saved .nc file (named as "NCMR_YYYY_MM_DD_THH_MM_SS.nc").""" @@ -187,10 +212,8 @@ def _makeDataCubes(self, filename) -> None: t = np.array(f.variables['T'][:]) # re-assign lons, lats to match heights - _lons = np.broadcast_to(lons[np.newaxis, np.newaxis, :], - t.shape) - _lats = np.broadcast_to(lats[np.newaxis, :, np.newaxis], - t.shape) + _lons = np.broadcast_to(lons[np.newaxis, np.newaxis, :], t.shape) + _lats = np.broadcast_to(lats[np.newaxis, :, np.newaxis], t.shape) # Re-structure everything from (heights, lats, lons) to (lons, lats, heights) _lats = np.transpose(_lats) diff --git a/tools/RAiDER/models/plotWeather.py b/tools/RAiDER/models/plotWeather.py index 1ef096877..c5bd8b29b 100755 --- a/tools/RAiDER/models/plotWeather.py +++ b/tools/RAiDER/models/plotWeather.py @@ -48,7 +48,9 @@ def plot_pqt(weatherObj, savefig=True, z1=500, z2=15000): # setup the plot f = plt.figure(figsize=(18, 14)) - f.suptitle(f'{weatherObj._Name} Pressure/Humidity/Temperature at height {z1}m and {z2}m (values should drop as elevation increases)') + f.suptitle( + f'{weatherObj._Name} Pressure/Humidity/Temperature at height {z1}m and {z2}m (values should drop as elevation increases)' + ) xind = int(np.floor(weatherObj._xs.shape[0] / 2)) yind = int(np.floor(weatherObj._ys.shape[0] / 2)) @@ -56,13 +58,15 @@ def plot_pqt(weatherObj, savefig=True, z1=500, z2=15000): # loop over each plot for ind, plot, title in zip(range(len(plots)), plots, titles): sp = f.add_subplot(3, 3, ind + 1) - im = sp.imshow(np.reshape(plot, x.shape), - cmap='viridis', - extent=[np.nanmin(x), np.nanmax(x), np.nanmin(y), np.nanmax(y)], - origin='lower') + im = sp.imshow( + np.reshape(plot, x.shape), + cmap='viridis', + extent=[np.nanmin(x), np.nanmax(x), np.nanmin(y), np.nanmax(y)], + origin='lower', + ) sp.plot(x[yind, xind], y[yind, xind], 'ko') divider = mal(sp) - cax = divider.append_axes("right", size="4%", pad=0.05) + cax = divider.append_axes('right', size='4%', pad=0.05) plt.colorbar(im, cax=cax) sp.set_title(title) if ind == 0: @@ -85,12 +89,11 @@ def plot_pqt(weatherObj, savefig=True, z1=500, z2=15000): sp.plot(weatherObj._t[yind, xind, :] - 273.15, zdata) sp.set_xlabel('Temp (C)') - plt.subplots_adjust(top=0.95, bottom=0.1, left=0.1, right=0.95, hspace=0.2, - wspace=0.3) + plt.subplots_adjust(top=0.95, bottom=0.1, left=0.1, right=0.95, hspace=0.2, wspace=0.3) if savefig: - wd = os.path.dirname(os.path.dirname(weatherObj._out_name)) - f = f'{weatherObj._Name}_weather_hgt{z1}_and_{z2}m.pdf' + wd = os.path.dirname(os.path.dirname(weatherObj._out_name)) + f = f'{weatherObj._Name}_weather_hgt{z1}_and_{z2}m.pdf' plt.savefig(os.path.join(wd, f)) return f @@ -101,8 +104,12 @@ def plot_wh(weatherObj, savefig=True, z1=500, z2=15000): at two different heights. """ # Get the interpolator - intFcn_w = Interpolator((weatherObj._xs, weatherObj._ys, weatherObj._zs), weatherObj._wet_refractivity.swapaxes(0, 1)) - intFcn_h = Interpolator((weatherObj._xs, weatherObj._ys, weatherObj._zs), weatherObj._hydrostatic_refractivity.swapaxes(0, 1)) + intFcn_w = Interpolator( + (weatherObj._xs, weatherObj._ys, weatherObj._zs), weatherObj._wet_refractivity.swapaxes(0, 1) + ) + intFcn_h = Interpolator( + (weatherObj._xs, weatherObj._ys, weatherObj._zs), weatherObj._hydrostatic_refractivity.swapaxes(0, 1) + ) # get the points needed XY = np.meshgrid(weatherObj._xs, weatherObj._ys) @@ -122,10 +129,7 @@ def plot_wh(weatherObj, savefig=True, z1=500, z2=15000): plots = [w1, h1, w2, h2] # titles - titles = (f'Wet refractivity {z1}', - f'Hydrostatic refractivity {z1}', - f'{z2}', - f'{z2}') + titles = (f'Wet refractivity {z1}', f'Hydrostatic refractivity {z1}', f'{z2}', f'{z2}') # setup the plot f = plt.figure(figsize=(14, 10)) @@ -134,10 +138,14 @@ def plot_wh(weatherObj, savefig=True, z1=500, z2=15000): # loop over each plot for ind, plot, title in zip(range(len(plots)), plots, titles): sp = f.add_subplot(2, 2, ind + 1) - im = sp.imshow(np.reshape(plot, x.shape), cmap='viridis', - extent=[np.nanmin(x), np.nanmax(x), np.nanmin(y), np.nanmax(y)], origin='lower') + im = sp.imshow( + np.reshape(plot, x.shape), + cmap='viridis', + extent=[np.nanmin(x), np.nanmax(x), np.nanmin(y), np.nanmax(y)], + origin='lower', + ) divider = mal(sp) - cax = divider.append_axes("right", size="4%", pad=0.05) + cax = divider.append_axes('right', size='4%', pad=0.05) plt.colorbar(im, cax=cax) sp.set_title(title) if ind == 0: @@ -146,7 +154,7 @@ def plot_wh(weatherObj, savefig=True, z1=500, z2=15000): sp.set_ylabel(f'{z2} m\n') if savefig: - wd = os.path.dirname(os.path.dirname(weatherObj._out_name)) - f = f'{weatherObj._Name}_refractivity_hgt{z1}_and_{z2}m.pdf' + wd = os.path.dirname(os.path.dirname(weatherObj._out_name)) + f = f'{weatherObj._Name}_refractivity_hgt{z1}_and_{z2}m.pdf' plt.savefig(os.path.join(wd, f)) return f diff --git a/tools/RAiDER/models/template.py b/tools/RAiDER/models/template.py index 905f26140..82ba5f0a5 100644 --- a/tools/RAiDER/models/template.py +++ b/tools/RAiDER/models/template.py @@ -19,8 +19,10 @@ def __init__(self) -> None: # Tuple of min/max years where data is available. # valid range of the dataset. Users need to specify the start date and end date (can be "present") - self._valid_range = (datetime.datetime(2016, 7, 15).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc)) + self._valid_range = ( + datetime.datetime(2016, 7, 15).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), + datetime.datetime.now(datetime.timezone.utc), + ) # Availability lag time. Can be specified in hours "hours=3" or in days "days=3" self._lag_time = datetime.timedelta(hours=3) # Availabile time resolution; i.e. minimum rate model is available in hours. 1 is hourly @@ -33,11 +35,11 @@ def __init__(self) -> None: self._k3 = 3.75e3 # [K^2/Pa] # horizontal grid spacing - self._lat_res = 3. / 111 # grid spacing in latitude - self._lon_res = 3. / 111 # grid spacing in longitude - self._x_res = 3. # x-direction grid spacing in the native weather model projection + self._lat_res = 3.0 / 111 # grid spacing in latitude + self._lon_res = 3.0 / 111 # grid spacing in longitude + self._x_res = 3.0 # x-direction grid spacing in the native weather model projection # (if the projection is in lat/lon, it is the same as "self._lon_res") - self._y_res = 3. # y-direction grid spacing in the weather model native projection + self._y_res = 3.0 # y-direction grid spacing in the weather model native projection # (if the projection is in lat/lon, it is the same as "self._lat_res") # zlevels specify fixed heights at which to interpolate the weather model variables @@ -56,7 +58,9 @@ def __init__(self) -> None: x0 = 0 y0 = 0 earth_radius = 6371229 - p1 = CRS(f'+proj=lcc +lat_1={lat1} +lat_2={lat2} +lat_0={lat0} +lon_0={lon0} +x_0={x0} +y_0={y0} +a={earth_radius} +b={earth_radius} +units=m +no_defs') + p1 = CRS( + f'+proj=lcc +lat_1={lat1} +lat_2={lat2} +lat_0={lat0} +lon_0={lon0} +x_0={x0} +y_0={y0} +a={earth_radius} +b={earth_radius} +units=m +no_defs' + ) self._proj = p1 def _fetch(self, out) -> None: diff --git a/tools/RAiDER/models/weatherModel.py b/tools/RAiDER/models/weatherModel.py index e8fe83a2b..f349262c3 100755 --- a/tools/RAiDER/models/weatherModel.py +++ b/tools/RAiDER/models/weatherModel.py @@ -20,14 +20,16 @@ from RAiDER.utilFcns import calcgeoh, clip_bbox, robmax, robmin, transform_coords -TIME_RES = {'GMAO': 3, - 'ECMWF': 1, - 'HRES': 6, - 'HRRR': 1, - 'WRF': 1, - 'NCMR': 1, - 'HRRR-AK': 3, - } +TIME_RES = { + 'GMAO': 3, + 'ECMWF': 1, + 'HRES': 6, + 'HRRR': 1, + 'WRF': 1, + 'NCMR': 1, + 'HRRR-AK': 3, +} + class WeatherModel(ABC): """Implement a generic weather model for getting estimated SAR delays.""" @@ -43,7 +45,7 @@ def __init__(self) -> None: self.files = None - self._time_res = None # time resolution of the weather model in hours + self._time_res = None # time resolution of the weather model in hours self._lon_res = None self._lat_res = None @@ -52,13 +54,13 @@ def __init__(self) -> None: self._classname = None self._dataset = None - self._Name = None - self._wmLoc = None + self._Name = None + self._wmLoc = None self._model_level_type = 'ml' self._valid_range = ( - datetime.datetime(1900, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), + datetime.datetime(1900, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), datetime.datetime.now(datetime.timezone.utc).date(), ) # Tuple of min/max years where data is available. self._lag_time = datetime.timedelta(days=30) # Availability lag time in days @@ -82,7 +84,7 @@ def __init__(self) -> None: self._lats = None self._lons = None self._ll_bounds = None - self._valid_bounds = box(-180, -90, 180, 90) # Shapely box with WSEN bounds + self._valid_bounds = box(-180, -90, 180, 90) # Shapely box with WSEN bounds self._p = None self._q = None @@ -94,7 +96,6 @@ def __init__(self) -> None: self._wet_ztd = None self._hydrostatic_ztd = None - def __str__(self) -> str: string = '\n' string += '======Weather Model class object=====\n' @@ -118,28 +119,22 @@ def __str__(self) -> str: string += 'Number of points in Lon/Lat = {}/{}\n'.format(*self._p.shape[:2]) string += f'Total number of grid points (3D): {np.prod(self._p.shape)}\n' if self._xs.size == 0: - string += f'Minimum/Maximum y: {robmin(self._ys): 4.2f}/{robmax(self._ys): 4.2f}\n'\ - - string += f'Minimum/Maximum x: {robmin(self._xs): 4.2f}/{robmax(self._xs): 4.2f}\n'\ - - string += f'Minimum/Maximum zs/heights: {robmin(self._zs): 10.2f}/{robmax(self._zs): 10.2f}\n'\ - + string += f'Minimum/Maximum y: {robmin(self._ys): 4.2f}/{robmax(self._ys): 4.2f}\n' + string += f'Minimum/Maximum x: {robmin(self._xs): 4.2f}/{robmax(self._xs): 4.2f}\n' + string += f'Minimum/Maximum zs/heights: {robmin(self._zs): 10.2f}/{robmax(self._zs): 10.2f}\n' + string += '=====================================\n' return string - def Model(self): return self._Name - def dtime(self): return self._time_res - def getLLRes(self): return np.max([self._lat_res, self._lon_res]) - def fetch(self, out, time) -> None: """ Checks the input datetime against the valid date range for the model and then @@ -161,17 +156,14 @@ def fetch(self, out, time) -> None: logger.exception(E) raise - @abstractmethod def _fetch(self, out): """Placeholder method. Should be implemented in each weather model type class.""" pass - def getTime(self): return self._time - def setTime(self, time, fmt='%Y-%m-%dT%H:%M:%S') -> None: """Set the time for a weather model.""" if isinstance(time, str): @@ -183,11 +175,9 @@ def setTime(self, time, fmt='%Y-%m-%dT%H:%M:%S') -> None: if self._time.tzinfo is None: self._time = self._time.replace(tzinfo=datetime.timezone(offset=datetime.timedelta())) - def get_latlon_bounds(self): return self._ll_bounds - def set_latlon_bounds(self, ll_bounds, Nextra=2, output_spacing=None) -> None: """ Need to correct lat/lon bounds because not all of the weather models have valid @@ -200,7 +190,7 @@ def set_latlon_bounds(self, ll_bounds, Nextra=2, output_spacing=None) -> None: ex_buffer_lon_max = 0.0 if self._Name in 'HRRR HRRR-AK HRES'.split(): - Nextra = 6 # have a bigger buffer + Nextra = 6 # have a bigger buffer else: ex_buffer_lon_max = self._lon_res @@ -209,18 +199,17 @@ def set_latlon_bounds(self, ll_bounds, Nextra=2, output_spacing=None) -> None: S, N, W, E = ll_bounds # Adjust bounds if they get near the poles or IDL - pixlat, pixlon = Nextra*self._lat_res, Nextra*self._lon_res + pixlat, pixlon = Nextra * self._lat_res, Nextra * self._lon_res - S= np.max([S - pixlat, -90.0 + pixlat]) - N= np.min([N + pixlat, 90.0 - pixlat]) - W= np.max([W - (pixlon + ex_buffer_lon_max), -180.0 + (pixlon+ex_buffer_lon_max)]) - E= np.min([E + (pixlon + ex_buffer_lon_max), 180.0 - pixlon - ex_buffer_lon_max]) + S = np.max([S - pixlat, -90.0 + pixlat]) + N = np.min([N + pixlat, 90.0 - pixlat]) + W = np.max([W - (pixlon + ex_buffer_lon_max), -180.0 + (pixlon + ex_buffer_lon_max)]) + E = np.min([E + (pixlon + ex_buffer_lon_max), 180.0 - pixlon - ex_buffer_lon_max]) if output_spacing is not None: - S, N, W, E = clip_bbox([S,N,W,E], output_spacing) + S, N, W, E = clip_bbox([S, N, W, E], output_spacing) self._ll_bounds = np.array([S, N, W, E]) - def get_wmLoc(self): """Get the path to the direct with the weather model files.""" if self._wmLoc is None: @@ -229,25 +218,18 @@ def get_wmLoc(self): wmLoc = self._wmLoc return wmLoc - - def set_wmLoc(self, weather_model_directory:str) -> None: + def set_wmLoc(self, weather_model_directory: str) -> None: """Set the path to the directory with the weather model files.""" self._wmLoc = weather_model_directory - - def load( - self, - *args, - _zlevels=None, - **kwargs - ): + def load(self, *args, _zlevels=None, **kwargs): """ Calls the load_weather method. Each model class should define a load_weather method appropriate for that class. 'args' should be one or more filenames. """ # If the weather file has already been processed, do nothing outLoc = self.get_wmLoc() - path_wm_raw = make_raw_weather_data_filename(outLoc, self.Model(), self.getTime()) + path_wm_raw = make_raw_weather_data_filename(outLoc, self.Model(), self.getTime()) self._out_name = self.out_file(outLoc) if os.path.exists(self._out_name): @@ -268,13 +250,11 @@ def load( self._getZTD() return None - @abstractmethod def load_weather(self, *args, **kwargs): """Placeholder method. Should be implemented in each weather model type class.""" pass - def plot(self, plotType='pqt', savefig=True): """Plotting method. Valid plot types are 'pqt'.""" if plotType == 'pqt': @@ -285,7 +265,6 @@ def plot(self, plotType='pqt', savefig=True): raise RuntimeError(f'WeatherModel.plot: No plotType named {plotType}') return plot - def checkTime(self, time) -> None: """ Checks the time against the lag time and valid date range for the given model type. @@ -301,27 +280,23 @@ def checkTime(self, time) -> None: if not isinstance(time, datetime.datetime): raise ValueError(f'"time" should be a Python datetime object, instead it is {time}') - - # This is needed because Python now gets angry if you try to compare non-timezone-aware - # objects with time-zone aware objects. + + # This is needed because Python now gets angry if you try to compare non-timezone-aware + # objects with time-zone aware objects. time = time.replace(tzinfo=datetime.timezone(offset=datetime.timedelta())) - logger.info( - 'Weather model %s is available from %s to %s', - self.Model(), start_time, end_time - ) + logger.info('Weather model %s is available from %s to %s', self.Model(), start_time, end_time) if time < start_time: raise DatetimeOutsideRange(self.Model(), time) if end_time < time: raise DatetimeOutsideRange(self.Model(), time) - # datetime.datetime.utcnow() is deprecated because Python developers - # want everyone to use timezone-aware datetimes. + # datetime.datetime.utcnow() is deprecated because Python developers + # want everyone to use timezone-aware datetimes. if time > datetime.datetime.now(datetime.timezone.utc) - self._lag_time: raise DatetimeOutsideRange(self.Model(), time) - def setLevelType(self, levelType) -> None: """Set the level type to model levels or pressure levels.""" if levelType in 'ml pl nat prs'.split(): @@ -334,18 +309,15 @@ def setLevelType(self, levelType) -> None: else: self.__pressure_levels__() - def _convertmb2Pa(self, pres): """Convert pressure in millibars to Pascals.""" return 100 * pres - def _get_heights(self, lats, geo_hgt, geo_ht_fill=np.nan) -> None: """Transform geo heights to WGS84 ellipsoidal heights.""" geo_ht_fix = np.where(geo_hgt != geo_ht_fill, geo_hgt, np.nan) - lats_full = np.broadcast_to(lats[...,np.newaxis], geo_ht_fix.shape) - self._zs = util.geo_to_ht(lats_full, geo_ht_fix) - + lats_full = np.broadcast_to(lats[..., np.newaxis], geo_ht_fix.shape) + self._zs = util.geo_to_ht(lats_full, geo_ht_fix) def _find_e(self) -> None: """Check the type of e-calculation needed.""" @@ -358,7 +330,6 @@ def _find_e(self) -> None: self._rh = None self._q = None - def _find_e_from_q(self) -> None: """Calculate e, partial pressure of water vapor.""" svp = find_svp(self._t) @@ -366,31 +337,25 @@ def _find_e_from_q(self) -> None: w = self._q / (1 - self._q) self._e = w * self._R_v * (self._p - svp) / self._R_d - def _find_e_from_rh(self) -> None: """Calculate partial pressure of water vapor.""" svp = find_svp(self._t) self._e = self._rh / 100 * svp - def _get_wet_refractivity(self) -> None: """Calculate the wet delay from pressure, temperature, and e.""" self._wet_refractivity = self._k2 * self._e / self._t + self._k3 * self._e / self._t**2 - def _get_hydro_refractivity(self) -> None: """Calculate the hydrostatic delay from pressure and temperature.""" self._hydrostatic_refractivity = self._k1 * self._p / self._t - def getWetRefractivity(self): return self._wet_refractivity - def getHydroRefractivity(self): return self._hydrostatic_refractivity - def _adjust_grid(self, ll_bounds=None) -> None: """ This function pads the weather grid with a level at self._zmin, if @@ -410,7 +375,6 @@ def _adjust_grid(self, ll_bounds=None) -> None: if ll_bounds is not None: self._trimExtent(ll_bounds) - def _getZTD(self) -> None: """ Compute the full slant tropospheric delay for each weather model grid node, using the reference @@ -422,16 +386,11 @@ def _getZTD(self) -> None: # Get the integrated ZTD wet_total, hydro_total = np.zeros(wet.shape), np.zeros(hydro.shape) for level in range(wet.shape[2]): - wet_total[..., level] = 1e-6 * np.trapz( - wet[..., level:], x=self._zs[level:], axis=2 - ) - hydro_total[..., level] = 1e-6 * np.trapz( - hydro[..., level:], x=self._zs[level:], axis=2 - ) + wet_total[..., level] = 1e-6 * np.trapz(wet[..., level:], x=self._zs[level:], axis=2) + hydro_total[..., level] = 1e-6 * np.trapz(hydro[..., level:], x=self._zs[level:], axis=2) self._hydrostatic_ztd = hydro_total self._wet_ztd = wet_total - def _getExtent(self, lats, lons): """Get the bounding box around a set of lats/lons.""" if (lats.size == 1) & (lons.size == 1): @@ -445,7 +404,6 @@ def _getExtent(self, lats, lons): else: raise RuntimeError('Not a valid lat/lon shape') - @property def bbox(self) -> list: """ @@ -474,11 +432,11 @@ def bbox(self) -> list: xmin, xmax = ds.longitude.min(), ds.longitude.max() ymin, ymax = ds.latitude.min(), ds.latitude.max() - wm_proj = self._proj - xs, ys = [xmin, xmin, xmax, xmax], [ymin, ymax, ymin, ymax] + wm_proj = self._proj + xs, ys = [xmin, xmin, xmax, xmax], [ymin, ymax, ymin, ymax] lons, lats = transform_coords(wm_proj, CRS(4326), xs, ys) - ## projected weather models may not be aligned N/S - ## should only matter for warning messages + # projected weather models may not be aligned N/S + # should only matter for warning messages W, E = np.min(lons), np.max(lons) # S, N = np.sort([lats[np.argmin(lons)], lats[np.argmax(lons)]]) S, N = np.min(lats), np.max(lats) @@ -486,11 +444,10 @@ def bbox(self) -> list: return self._bbox - def checkValidBounds( - self: weatherModel, - ll_bounds: np.ndarray, - ): + self: weatherModel, + ll_bounds: np.ndarray, + ): """ Checks whether the given bounding box is valid for the model (i.e., intersects with the model domain at all). @@ -510,11 +467,8 @@ def checkValidBounds( return Mod - - def checkContainment(self: weatherModel, - ll_bounds, - buffer_deg: float = 1e-5) -> bool: - """" + def checkContainment(self: weatherModel, ll_bounds, buffer_deg: float = 1e-5) -> bool: + """ " Checks containment of weather model bbox of outLats and outLons provided. @@ -534,40 +488,36 @@ def checkContainment(self: weatherModel, and False otherwise. """ ymin_input, ymax_input, xmin_input, xmax_input = ll_bounds - input_box = box(xmin_input, ymin_input, xmax_input, ymax_input) + input_box = box(xmin_input, ymin_input, xmax_input, ymax_input) xmin, ymin, xmax, ymax = self.bbox weather_model_box = box(xmin, ymin, xmax, ymax) - world_box = box(-180, -90, 180, 90) + world_box = box(-180, -90, 180, 90) # Logger - input_box_str = [f'{x:1.2f}' for x in [xmin_input, ymin_input, - xmax_input, ymax_input]] + input_box_str = [f'{x:1.2f}' for x in [xmin_input, ymin_input, xmax_input, ymax_input]] weath_box_str = [f'{x:1.2f}' for x in [xmin, ymin, xmax, ymax]] weath_box_str = ', '.join(weath_box_str) input_box_str = ', '.join(input_box_str) - logger.info(f'Extent of the weather model is (xmin, ymin, xmax, ymax):' - f'{weath_box_str}') - logger.info(f'Extent of the input is (xmin, ymin, xmax, ymax): ' - f'{input_box_str}') + logger.info(f'Extent of the weather model is (xmin, ymin, xmax, ymax):' f'{weath_box_str}') + logger.info(f'Extent of the input is (xmin, ymin, xmax, ymax): ' f'{input_box_str}') # If the bounding box goes beyond the normal world extents # Look at two x-translates, buffer them, and take their union. if not world_box.contains(weather_model_box): - logger.info('Considering x-translates of weather model +/-360 ' - 'as bounding box outside of -180, -90, 180, 90') - translates = [weather_model_box.buffer(buffer_deg), - translate(weather_model_box, - xoff=360).buffer(buffer_deg), - translate(weather_model_box, - xoff=-360).buffer(buffer_deg) - ] + logger.info( + 'Considering x-translates of weather model +/-360 as bounding box outside of -180, -90, 180, 90' + ) + translates = [ + weather_model_box.buffer(buffer_deg), + translate(weather_model_box, xoff=360).buffer(buffer_deg), + translate(weather_model_box, xoff=-360).buffer(buffer_deg), + ] weather_model_box = unary_union(translates) return weather_model_box.contains(input_box) - def _isOutside(self, extent1, extent2) -> bool: """ Determine whether any of extent1 lies outside extent2. @@ -579,15 +529,13 @@ def _isOutside(self, extent1, extent2) -> bool: t4 = extent1[3] > extent2[3] return np.any([t1, t2, t3, t4]) - def _trimExtent(self, extent) -> None: """Get the bounding box around a set of lats/lons.""" lat = self._lats.copy() lon = self._lons.copy() lat[np.isnan(lat)] = np.nanmean(lat) lon[np.isnan(lon)] = np.nanmean(lon) - mask = (lat >= extent[0]) & (lat <= extent[1]) & \ - (lon >= extent[2]) & (lon <= extent[3]) + mask = (lat >= extent[0]) & (lat <= extent[1]) & (lon >= extent[2]) & (lon <= extent[3]) ma1 = np.sum(mask, axis=1).astype('bool') ma2 = np.sum(mask, axis=0).astype('bool') if np.sum(ma1) == 0 and np.sum(ma2) == 0: @@ -613,7 +561,6 @@ def _trimExtent(self, extent) -> None: self._wet_refractivity = self._wet_refractivity[index1:index2, index3:index4, ...] self._hydrostatic_refractivity = self._hydrostatic_refractivity[index1:index2, index3:index4, :] - def _calculategeoh(self, z, lnsp): """ Function to calculate pressure, geopotential, and geopotential height @@ -631,16 +578,13 @@ def _calculategeoh(self, z, lnsp): """ return calcgeoh(lnsp, self._t, self._q, z, self._a, self._b, self._R_d, self._levels) - def getProjection(self): """Returns: the native weather projection, which should be a pyproj object.""" return self._proj - def getPoints(self): return self._xs.copy(), self._ys.copy(), self._zs.copy() - def _uniform_in_z(self, _zlevels=None) -> None: """Interpolate all variables to a regular grid in z.""" nx, ny = self._p.shape[:2] @@ -655,28 +599,20 @@ def _uniform_in_z(self, _zlevels=None) -> None: new_zs = np.tile(_zlevels, (nx, ny, 1)) # re-assign values to the uniform z - self._t = interpolate_along_axis( - self._zs, self._t, new_zs, axis=2, fill_value=np.nan - ).astype(np.float32) - self._p = interpolate_along_axis( - self._zs, self._p, new_zs, axis=2, fill_value=np.nan - ).astype(np.float32) - self._e = interpolate_along_axis( - self._zs, self._e, new_zs, axis=2, fill_value=np.nan - ).astype(np.float32) + self._t = interpolate_along_axis(self._zs, self._t, new_zs, axis=2, fill_value=np.nan).astype(np.float32) + self._p = interpolate_along_axis(self._zs, self._p, new_zs, axis=2, fill_value=np.nan).astype(np.float32) + self._e = interpolate_along_axis(self._zs, self._e, new_zs, axis=2, fill_value=np.nan).astype(np.float32) self._zs = _zlevels self._xs = np.unique(self._xs) self._ys = np.unique(self._ys) - def _checkForNans(self) -> None: """Fill in NaN-values.""" self._p = fillna3D(self._p) - self._t = fillna3D(self._t, fill_value=1e16) # to avoid division by zero later on + self._t = fillna3D(self._t, fill_value=1e16) # to avoid division by zero later on self._e = fillna3D(self._e) - def out_file(self, outLoc): f = make_weather_model_filename( self._Name, @@ -685,7 +621,6 @@ def out_file(self, outLoc): ) return os.path.join(outLoc, f) - def filename(self, time=None, outLoc='weather_files'): """Create a filename to store the weather model.""" os.makedirs(outLoc, exist_ok=True) @@ -705,7 +640,6 @@ def filename(self, time=None, outLoc='weather_files'): self.files = [f] return f - def write(self): """ By calling the abstract/modular netcdf writer @@ -716,13 +650,12 @@ def write(self): f = self._out_name attrs_dict = { - "Conventions": 'CF-1.6', - "datetime": datetime.datetime.strftime(self._time, "%Y_%m_%dT%H_%M_%S"), - 'date_created': datetime.datetime.now().strftime("%Y_%m_%dT%H_%M_%S"), - 'title': 'Weather model data and delay calculations', - 'model_name': self._Name - - } + 'Conventions': 'CF-1.6', + 'datetime': datetime.datetime.strftime(self._time, '%Y_%m_%dT%H_%M_%S'), + 'date_created': datetime.datetime.now().strftime('%Y_%m_%dT%H_%M_%S'), + 'title': 'Weather model data and delay calculations', + 'model_name': self._Name, + } dimension_dict = { 'x': ('x', self._xs), @@ -764,7 +697,7 @@ def write(self): ds['hydro_total'].attrs['standard_name'] = 'total_hydrostatic_refractivity' # projection information - ds["proj"] = 0 + ds['proj'] = 0 for k, v in self._proj.to_cf().items(): ds.proj.attrs[k] = v for var in ds.data_vars: @@ -777,29 +710,23 @@ def write(self): def make_weather_model_filename(name, time, ll_bounds) -> str: s = np.floor(ll_bounds[0]) - S = f'{np.abs(s):.0f}S' if s <0 else f'{s:.0f}N' + S = f'{np.abs(s):.0f}S' if s < 0 else f'{s:.0f}N' n = np.ceil(ll_bounds[1]) - N = f'{np.abs(n):.0f}S' if n <0 else f'{n:.0f}N' + N = f'{np.abs(n):.0f}S' if n < 0 else f'{n:.0f}N' w = np.floor(ll_bounds[2]) - W = f'{np.abs(w):.0f}W' if w <0 else f'{w:.0f}E' + W = f'{np.abs(w):.0f}W' if w < 0 else f'{w:.0f}E' e = np.ceil(ll_bounds[3]) - E = f'{np.abs(e):.0f}W' if e <0 else f'{e:.0f}E' + E = f'{np.abs(e):.0f}W' if e < 0 else f'{e:.0f}E' return f'{name}_{time.strftime("%Y_%m_%d_T%H_%M_%S")}_{S}_{N}_{W}_{E}.nc' def make_raw_weather_data_filename(outLoc, name, time): """Filename generator for the raw downloaded weather model data.""" - f = os.path.join( - outLoc, - '{}_{}.{}'.format( - name, - datetime.datetime.strftime(time, '%Y_%m_%d_T%H_%M_%S'), - 'nc' - ) - ) + date_string = datetime.datetime.strftime(time, '%Y_%m_%d_T%H_%M_%S') + f = os.path.join(outLoc, f'{name}_{date_string}.nc') return f @@ -823,8 +750,8 @@ def find_svp(t): tref = t - t1 wgt = (t - t2) / (t1 - t2) - svpw = (6.1121 * np.exp((17.502 * tref) / (240.97 + tref))) - svpi = (6.1121 * np.exp((22.587 * tref) / (273.86 + tref))) + svpw = 6.1121 * np.exp((17.502 * tref) / (240.97 + tref)) + svpi = 6.1121 * np.exp((22.587 * tref) / (273.86 + tref)) svp = svpi + (svpw - svpi) * wgt**2 ix_bound1 = t > t1 @@ -839,16 +766,14 @@ def find_svp(t): def get_mapping(proj): """Get CF-complient projection information from a proj.""" # In case of WGS-84 lat/lon, keep it simple - if proj.to_epsg()==4326: + if proj.to_epsg() == 4326: return 'WGS84' else: return proj.to_wkt() -def checkContainment_raw(path_wm_raw, - ll_bounds, - buffer_deg: float = 1e-5) -> bool: - """" +def checkContainment_raw(path_wm_raw, ll_bounds, buffer_deg: float = 1e-5) -> bool: + """ " Checks if existing raw weather model contains requested ll_bounds. @@ -868,8 +793,9 @@ def checkContainment_raw(path_wm_raw, and False otherwise. """ import xarray as xr + ymin_input, ymax_input, xmin_input, xmax_input = ll_bounds - input_box = box(xmin_input, ymin_input, xmax_input, ymax_input) + input_box = box(xmin_input, ymin_input, xmax_input, ymax_input) with xr.open_dataset(path_wm_raw) as ds: try: @@ -879,31 +805,27 @@ def checkContainment_raw(path_wm_raw, ymin, ymax = ds.y.min(), ds.y.max() xmin, xmax = ds.x.min(), ds.x.max() - xmin, xmax = np.mod(np.array([xmin, xmax])+180, 360) - 180 + xmin, xmax = np.mod(np.array([xmin, xmax]) + 180, 360) - 180 weather_model_box = box(xmin, ymin, xmax, ymax) - world_box = box(-180, -90, 180, 90) + world_box = box(-180, -90, 180, 90) # Logger - input_box_str = [f'{x:1.2f}' for x in [xmin_input, ymin_input, - xmax_input, ymax_input]] + input_box_str = [f'{x:1.2f}' for x in [xmin_input, ymin_input, xmax_input, ymax_input]] weath_box_str = [f'{x:1.2f}' for x in [xmin, ymin, xmax, ymax]] weath_box_str = ', '.join(weath_box_str) input_box_str = ', '.join(input_box_str) - # If the bounding box goes beyond the normal world extents # Look at two x-translates, buffer them, and take their union. if not world_box.contains(weather_model_box): - logger.info('Considering x-translates of weather model +/-360 ' - 'as bounding box outside of -180, -90, 180, 90') - translates = [weather_model_box.buffer(buffer_deg), - translate(weather_model_box, - xoff=360).buffer(buffer_deg), - translate(weather_model_box, - xoff=-360).buffer(buffer_deg) - ] + logger.info('Considering x-translates of weather model +/-360 as bounding box outside of -180, -90, 180, 90') + translates = [ + weather_model_box.buffer(buffer_deg), + translate(weather_model_box, xoff=360).buffer(buffer_deg), + translate(weather_model_box, xoff=-360).buffer(buffer_deg), + ] weather_model_box = unary_union(translates) return weather_model_box.contains(input_box) diff --git a/tools/RAiDER/models/wrf.py b/tools/RAiDER/models/wrf.py index 5fe81fbb1..5a64febf5 100644 --- a/tools/RAiDER/models/wrf.py +++ b/tools/RAiDER/models/wrf.py @@ -16,6 +16,7 @@ # class WRF(WeatherModel): """WRF class definition, based on the WeatherModel base class.""" + # TODO: finish implementing def __init__(self) -> None: @@ -25,7 +26,6 @@ def __init__(self) -> None: self._k2 = 0.233 # K/Pa self._k3 = 3.75e3 # K^2/Pa - # Currently WRF is using RH instead of Q to get E self._humidityType = 'rh' self._Name = 'WRF' @@ -57,10 +57,8 @@ def load_weather(self, file1, file2, *args, **kwargs) -> None: xs = np.mean(xs, axis=0) ys = np.mean(ys, axis=1) - _xs = np.broadcast_to(xs[np.newaxis, np.newaxis, :], - self._p.shape) - _ys = np.broadcast_to(ys[np.newaxis, :, np.newaxis], - self._p.shape) + _xs = np.broadcast_to(xs[np.newaxis, np.newaxis, :], self._p.shape) + _ys = np.broadcast_to(ys[np.newaxis, :, np.newaxis], self._p.shape) # Re-structure everything from (heights, lats, lons) to (lons, lats, heights) self._p = np.transpose(self._p) self._t = np.transpose(self._t) @@ -123,10 +121,17 @@ def _read_netcdf(self, weatherFile, defNul=None) -> None: # Projection # See http://www.pkrc.net/wrf-lambert.html earthRadius = 6370e3 # <- note Ray had a bug here - p1 = CRS(proj='lcc', lat_1=lat1, - lat_2=lat2, lat_0=lat0, - lon_0=lon0, a=earthRadius, b=earthRadius, - towgs84=(0, 0, 0), no_defs=True) + p1 = CRS( + proj='lcc', + lat_1=lat1, + lat_2=lat2, + lat_0=lat0, + lon_0=lon0, + a=earthRadius, + b=earthRadius, + towgs84=(0, 0, 0), + no_defs=True, + ) self._proj = p1 temps[temps == tNull] = np.nan @@ -149,14 +154,14 @@ def _read_netcdf(self, weatherFile, defNul=None) -> None: self._zs = geoh if len(sp.shape) == 1: - self._p = np.broadcast_to( - sp[:, np.newaxis, np.newaxis], self._zs.shape) + self._p = np.broadcast_to(sp[:, np.newaxis, np.newaxis], self._zs.shape) else: self._p = sp class UnitTypeError(Exception): """Define a unit type exception for easily formatting error messages for units.""" + def __init___(self, varName, unittype): msg = f"Unknown units for {varName}: '{unittype}'" Exception.__init__(self, msg) diff --git a/tools/RAiDER/processWM.py b/tools/RAiDER/processWM.py index 9474d920d..f9cc75fdd 100755 --- a/tools/RAiDER/processWM.py +++ b/tools/RAiDER/processWM.py @@ -24,9 +24,9 @@ def prepareWeatherModel( weather_model, time, ll_bounds, - download_only: bool=False, - makePlots: bool=False, - force_download: bool=False, + download_only: bool = False, + makePlots: bool = False, + force_download: bool = False, ) -> str: """Parse inputs to download and prepare a weather model grid for interpolation. @@ -41,12 +41,12 @@ def prepareWeatherModel( Returns: str: filename of the netcdf file to which the weather model has been written """ - ## set the bounding box from the in the case that it hasn't been set + # set the bounding box from the in the case that it hasn't been set if weather_model.get_latlon_bounds() is None: weather_model.set_latlon_bounds(ll_bounds) # Ensure the file output location exists - wmLoc = weather_model.get_wmLoc() + wmLoc = weather_model.get_wmLoc() weather_model.setTime(time) # get the path to the less processed weather model file @@ -58,15 +58,16 @@ def prepareWeatherModel( # check whether weather model files exists and/or or should be downloaded if os.path.exists(path_wm_crop) and not force_download: logger.warning( - 'Processed weather model already exists, please remove it ("%s") if you want ' - 'to download a new one.', path_wm_crop) + 'Processed weather model already exists, please remove it ("%s") if you want to download a new one.', + path_wm_crop, + ) # check whether the raw weather model covers this area - elif os.path.exists(path_wm_raw) and \ - checkContainment_raw(path_wm_raw, ll_bounds) and not force_download: + elif os.path.exists(path_wm_raw) and checkContainment_raw(path_wm_raw, ll_bounds) and not force_download: logger.warning( - 'Raw weather model already exists, please remove it ("%s") if you want ' - 'to download a new one.', path_wm_raw) + 'Raw weather model already exists, please remove it ("%s") if you want to download a new one.', + path_wm_raw, + ) # if no weather model files supplied, check the standard location else: @@ -78,19 +79,14 @@ def prepareWeatherModel( # If only downloading, exit now if download_only: - logger.warning( - 'download_only flag selected. No further processing will happen.' - ) + logger.warning('download_only flag selected. No further processing will happen.') return None # Otherwise, load the weather model data f = weather_model.load() if f is not None: - logger.warning( - 'The processed weather model file already exists,' - ' so I will use that.' - ) + logger.warning('The processed weather model file already exists, so I will use that.') containment = weather_model.checkContainment(ll_bounds) if not containment and weather_model.Model() not in 'HRRR'.split(): @@ -99,26 +95,19 @@ def prepareWeatherModel( return f # Logging some basic info - logger.debug( - 'Number of weather model nodes: %s', - np.prod(weather_model.getWetRefractivity().shape) - ) + logger.debug('Number of weather model nodes: %s', np.prod(weather_model.getWetRefractivity().shape)) shape = weather_model.getWetRefractivity().shape logger.debug(f'Shape of weather model: {shape}') logger.debug( 'Bounds of the weather model: %.2f/%.2f/%.2f/%.2f (SNWE)', - np.nanmin(weather_model._ys), np.nanmax(weather_model._ys), - np.nanmin(weather_model._xs), np.nanmax(weather_model._xs) + np.nanmin(weather_model._ys), + np.nanmax(weather_model._ys), + np.nanmin(weather_model._xs), + np.nanmax(weather_model._xs), ) logger.debug('Weather model: %s', weather_model.Model()) - logger.debug( - 'Mean value of the wet refractivity: %f', - np.nanmean(weather_model.getWetRefractivity()) - ) - logger.debug( - 'Mean value of the hydrostatic refractivity: %f', - np.nanmean(weather_model.getHydroRefractivity()) - ) + logger.debug('Mean value of the wet refractivity: %f', np.nanmean(weather_model.getWetRefractivity())) + logger.debug('Mean value of the hydrostatic refractivity: %f', np.nanmean(weather_model.getHydroRefractivity())) logger.debug(weather_model) if makePlots: @@ -131,7 +120,7 @@ def prepareWeatherModel( containment = weather_model.checkContainment(ll_bounds) except Exception as e: - logger.exception("Unable to save weathermodel to file") + logger.exception('Unable to save weathermodel to file') logger.exception(e) raise CriticalError @@ -145,17 +134,7 @@ def prepareWeatherModel( return f -def _weather_model_debug( - los, - lats, - lons, - ll_bounds, - weather_model, - wmLoc, - time, - out, - download_only -) -> None: +def _weather_model_debug(los, lats, lons, ll_bounds, weather_model, wmLoc, time, out, download_only) -> None: """RaiderWeatherModelDebug main function.""" logger.debug('Starting to run the weather model calculation with debugging plots') logger.debug('Time type: %s', type(time)) @@ -168,11 +147,7 @@ def _weather_model_debug( wmLoc = os.path.join(out, 'weather_files') # weather model calculation - wm_filename = make_weather_model_filename( - weather_model['name'], - time, - ll_bounds - ) + wm_filename = make_weather_model_filename(weather_model['name'], time, ll_bounds) weather_model_file = os.path.join(wmLoc, wm_filename) if not os.path.exists(weather_model_file): @@ -184,9 +159,9 @@ def _weather_model_debug( lons=lons, ll_bounds=ll_bounds, download_only=download_only, - makePlots=True + makePlots=True, ) try: weather_model.write2NETCDF4(weather_model_file) except Exception: - logger.exception("Unable to save weathermodel to file") + logger.exception('Unable to save weathermodel to file') diff --git a/tools/RAiDER/s1_azimuth_timing.py b/tools/RAiDER/s1_azimuth_timing.py index 368aa78f2..164fdefda 100644 --- a/tools/RAiDER/s1_azimuth_timing.py +++ b/tools/RAiDER/s1_azimuth_timing.py @@ -16,10 +16,12 @@ from RAiDER.s1_orbits import get_orbits_from_slc_ids_hyp3lib -def _asf_query(point: Point, - start: datetime.datetime, - end: datetime.datetime, - buffer_degrees: float = 2) -> list[str]: +def _asf_query( + point: Point, + start: datetime.datetime, + end: datetime.datetime, + buffer_degrees: float = 2 +) -> list[str]: """ Using a buffer to get as many SLCs covering a given request as. @@ -34,21 +36,24 @@ def _asf_query(point: Point, ------- list[str] """ - results = asf.geo_search(intersectsWith=point.buffer(buffer_degrees).wkt, - processingLevel=asf.PRODUCT_TYPE.SLC, - start=start, - end=end, - maxResults=5 - ) + results = asf.geo_search( + intersectsWith=point.buffer(buffer_degrees).wkt, + processingLevel=asf.PRODUCT_TYPE.SLC, + start=start, + end=end, + maxResults=5, + ) slc_ids = [r.properties['sceneName'] for r in results] return slc_ids -def get_slc_id_from_point_and_time(lon: float, - lat: float, - dt: datetime.datetime, - buffer_seconds: int = 600, - buffer_deg: float = 2) -> list: +def get_slc_id_from_point_and_time( + lon: float, + lat: float, + dt: datetime.datetime, + buffer_seconds: int = 600, + buffer_deg: float = 2 +) -> list: """ Obtains a (non-unique) SLC id from the lon/lat and datetime of inputs. The buffere ensures that an SLC id is within the queried start/end times. Note an S1 scene takes roughly 30 seconds to acquire. @@ -80,10 +85,12 @@ def get_slc_id_from_point_and_time(lon: float, return slc_ids -def get_azimuth_time_grid(lon_mesh: np.ndarray, - lat_mesh: np.ndarray, - hgt_mesh: np.ndarray, - orb: 'isce.core.Orbit') -> np.ndarray: +def get_azimuth_time_grid( + lon_mesh: np.ndarray, + lat_mesh: np.ndarray, + hgt_mesh: np.ndarray, + orb: 'isce.core.Orbit' +) -> np.ndarray: """ Source: https://github.com/dbekaert/RAiDER/blob/dev/tools/RAiDER/losreader.py#L601C1-L674C22 @@ -102,28 +109,35 @@ def get_azimuth_time_grid(lon_mesh: np.ndarray, look = isce.core.LookSide.Right m, n, p = hgt_mesh.shape - az_arr = np.full((m, n, p), - np.datetime64('NaT'), - # source: https://stackoverflow.com/a/27469108 - dtype='datetime64[ms]') + az_arr = np.full( + (m, n, p), + np.datetime64('NaT'), + # source: https://stackoverflow.com/a/27469108 + dtype='datetime64[ms]', + ) for ind_0 in range(m): for ind_1 in range(n): for ind_2 in range(p): + hgt_pt, lat_pt, lon_pt = ( + hgt_mesh[ind_0, ind_1, ind_2], + lat_mesh[ind_0, ind_1, ind_2], + lon_mesh[ind_0, ind_1, ind_2], + ) - hgt_pt, lat_pt, lon_pt = (hgt_mesh[ind_0, ind_1, ind_2], - lat_mesh[ind_0, ind_1, ind_2], - lon_mesh[ind_0, ind_1, ind_2]) - - input_vec = np.array([np.deg2rad(lon_pt), - np.deg2rad(lat_pt), - hgt_pt]) + input_vec = np.array([np.deg2rad(lon_pt), np.deg2rad(lat_pt), hgt_pt]) aztime, sr = isce.geometry.geo2rdr( - input_vec, elp, orb, dop, 0.06, look, + input_vec, + elp, + orb, + dop, + 0.06, + look, threshold=residual_threshold, maxiter=num_iteration, - delta_range=10.0) + delta_range=10.0, + ) rng_seconds = sr / isce.core.speed_of_light aztime = aztime + rng_seconds @@ -133,10 +147,12 @@ def get_azimuth_time_grid(lon_mesh: np.ndarray, return az_arr -def get_s1_azimuth_time_grid(lon: np.ndarray, - lat: np.ndarray, - hgt: np.ndarray, - dt: datetime.datetime) -> np.ndarray: +def get_s1_azimuth_time_grid( + lon: np.ndarray, + lat: np.ndarray, + hgt: np.ndarray, + dt: datetime.datetime +) -> np.ndarray: """Based on the lon, lat, hgt (3d cube) - obtains an associated s1 orbit file to calculate the azimuth timing across the cube. Requires datetime of acq associated to cube. @@ -163,12 +179,16 @@ def get_s1_azimuth_time_grid(lon: np.ndarray, raise ValueError('Coordinates must be 1d or 3d coordinate arrays') if dims[0] == 1: - hgt_mesh, lat_mesh, lon_mesh = np.meshgrid(hgt, lat, lon, - # indexing keyword argument - # Ensures output dimensions - # align with order the inputs - # height x latitude x longitude - indexing='ij') + hgt_mesh, lat_mesh, lon_mesh = np.meshgrid( + hgt, + lat, + lon, + # indexing keyword argument + # Ensures output dimensions + # align with order the inputs + # height x latitude x longitude + indexing='ij', + ) else: hgt_mesh = hgt lat_mesh = lat @@ -181,9 +201,7 @@ def get_s1_azimuth_time_grid(lon: np.ndarray, except ValueError: warnings.warn('No slc id found for the given datetime and grid; returning empty grid') m, n, p = hgt_mesh.shape - az_arr = np.full((m, n, p), - np.datetime64('NaT'), - dtype='datetime64[ms]') + az_arr = np.full((m, n, p), np.datetime64('NaT'), dtype='datetime64[ms]') return az_arr orb_files = get_orbits_from_slc_ids_hyp3lib(slc_ids) @@ -195,9 +213,11 @@ def get_s1_azimuth_time_grid(lon: np.ndarray, return az_arr -def get_n_closest_datetimes(ref_time: datetime.datetime, - n_target_times: int, - time_step_hours: int) -> list[datetime.datetime]: +def get_n_closest_datetimes( + ref_time: datetime.datetime, + n_target_times: int, + time_step_hours: int +) -> list[datetime.datetime]: """ Gets n closest times relative to the `round_to_hour_delta` and the `ref_time`. Specifically, if one is interetsted in getting 3 closest times @@ -234,9 +254,11 @@ def get_n_closest_datetimes(ref_time: datetime.datetime, closest_times = [] if (24 % time_step_hours) != 0: - raise ValueError('The time step does not evenly divide 24 hours;' - 'Time step has period > 1 day and depends when model ' - 'starts') + raise ValueError( + 'The time step does not evenly divide 24 hours;' + 'Time step has period > 1 day and depends when model ' + 'starts' + ) ts = pd.Timestamp(ref_time) for k in range(iterations): @@ -255,9 +277,11 @@ def get_n_closest_datetimes(ref_time: datetime.datetime, return closest_times -def get_times_for_azimuth_interpolation(ref_time: datetime.datetime, - time_step_hours: int, - buffer_in_seconds: int = 300) -> list[datetime.datetime]: +def get_times_for_azimuth_interpolation( + ref_time: datetime.datetime, + time_step_hours: int, + buffer_in_seconds: int = 300 +) -> list[datetime.datetime]: """Obtains times needed for azimuth interpolation. Filters 3 closests dates from ref_time so that all returned dates are within `time_step_hours` + `buffer_in_seconds`. @@ -304,14 +328,17 @@ def filter_time(time: datetime.datetime): absolute_time_difference_sec = abs((ref_time - time).total_seconds()) upper_bound_seconds = time_step_hours * 60 * 60 + buffer_in_seconds return absolute_time_difference_sec < upper_bound_seconds + out_times = list(filter(filter_time, closest_times)) return out_times -def get_inverse_weights_for_dates(azimuth_time_array: np.ndarray, - dates: list[datetime.datetime], - inverse_regularizer: float = 1e-9, - temporal_window_hours: float = None) -> list[np.ndarray]: +def get_inverse_weights_for_dates( + azimuth_time_array: np.ndarray, + dates: list[datetime.datetime], + inverse_regularizer: float = 1e-9, + temporal_window_hours: float = None, +) -> list[np.ndarray]: """Obtains weights according to inverse weighting with respect to the absolute difference between azimuth timing array and dates. The output will be a list with length equal to that of dates and whose entries are arrays each whose shape matches the azimuth_timing_array. @@ -358,7 +385,7 @@ def get_inverse_weights_for_dates(azimuth_time_array: np.ndarray, abs_diff = [np.abs(azimuth_time_array - date) / np.timedelta64(1, 's') for date in dates_np] # Get inverse weighting with mask determined by window - wgts = [1. / (diff + inverse_regularizer) for diff in abs_diff] + wgts = [1.0 / (diff + inverse_regularizer) for diff in abs_diff] masks = [(diff <= temporal_window_seconds).astype(int) for diff in abs_diff] if all([mask.sum() == 0 for mask in masks]): diff --git a/tools/RAiDER/s1_orbits.py b/tools/RAiDER/s1_orbits.py index 6607e3f12..e88bc807d 100644 --- a/tools/RAiDER/s1_orbits.py +++ b/tools/RAiDER/s1_orbits.py @@ -45,9 +45,11 @@ def ensure_orbit_credentials() -> Optional[int]: username = os.environ.get('ESA_USERNAME') password = os.environ.get('ESA_PASSWORD') if username is None or password is None: - raise ValueError('Credentials are required for fetching orbit data from dataspace.copernicus.eu!\n' - 'Either add your credentials to ~/.netrc or set the ESA_USERNAME and ESA_PASSWORD ' - 'environment variables.') + raise ValueError( + 'Credentials are required for fetching orbit data from dataspace.copernicus.eu!\n' + 'Either add your credentials to ~/.netrc or set the ESA_USERNAME and ESA_PASSWORD ' + 'environment variables.' + ) netrc_credentials.hosts[ESA_CDSE_HOST] = (username, None, password) @@ -55,9 +57,11 @@ def ensure_orbit_credentials() -> Optional[int]: username = os.environ.get('EARTHDATA_USERNAME') password = os.environ.get('EARTHDATA_PASSWORD') if username is None or password is None: - raise ValueError('Credentials are required for fetching orbit data from s1qc.asf.alaska.edu!\n' - 'Either add your credentials to ~/.netrc or set the EARTHDATA_USERNAME and' - ' EARTHDATA_PASSWORD environment variables.') + raise ValueError( + 'Credentials are required for fetching orbit data from s1qc.asf.alaska.edu!\n' + 'Either add your credentials to ~/.netrc or set the EARTHDATA_USERNAME and' + ' EARTHDATA_PASSWORD environment variables.' + ) netrc_credentials.hosts[NASA_EDL_HOST] = (username, None, password) @@ -80,9 +84,7 @@ def get_orbits_from_slc_ids(slc_ids: List[str], directory=Path.cwd()) -> List[Pa return orb_files -def get_orbits_from_slc_ids_hyp3lib( - slc_ids: list, orbit_directory: str = None -) -> dict: +def get_orbits_from_slc_ids_hyp3lib(slc_ids: list, orbit_directory: str = None) -> dict: """Reference: https://github.com/ACCESS-Cloud-Based-InSAR/DockerizedTopsApp/blob/dev/isce2_topsapp/localize_orbits.py#L23.""" # Populates env variables to netrc as required for sentineleof _ = ensure_orbit_credentials() diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index bdf6e13ed..5ef280004 100755 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -1,4 +1,5 @@ """Geodesy-related utility functions.""" + import os import re from datetime import datetime, timedelta, timezone @@ -43,7 +44,7 @@ def projectDelays(delay, inc): """Project zenith delays to LOS.""" - if inc==90: + if inc == 90: raise ZeroDivisionError return delay / cosd(inc) @@ -123,8 +124,8 @@ def rio_profile(fname): """Reads the profile of a rasterio file.""" if rasterio is None: raise ImportError('RAiDER.utilFcns: rio_profile - rasterio is not installed') - - ## need to access subdataset directly + + # need to access subdataset directly if os.path.basename(fname).startswith('S1-GUNW'): fname = os.path.join(f'NETCDF:"{fname}":science/grids/data/unwrappedPhase') with rasterio.open(fname) as src: @@ -141,9 +142,9 @@ def rio_profile(fname): def rio_extents(profile): """Get a bounding box in SNWE from a rasterio profile.""" - gt = profile["transform"].to_gdal() - xSize = profile["width"] - ySize = profile["height"] + gt = profile['transform'].to_gdal() + xSize = profile['width'] + ySize = profile['height'] W, E = gt[0], gt[0] + (xSize - 1) * gt[1] + (ySize - 1) * gt[2] N, S = gt[3], gt[3] + (xSize - 1) * gt[4] + (ySize - 1) * gt[5] return S, N, W, E @@ -178,11 +179,10 @@ def rio_open(fname, returnProj=False, userNDV=None, band=None): else: nodataToNan(data, list(nodata) + [userNDV]) - if data.ndim > 2: dlist = [] for k in range(data.shape[0]): - dlist.append(data[k,...].copy()) + dlist.append(data[k, ...].copy()) data = dlist if not returnProj: @@ -194,7 +194,7 @@ def rio_open(fname, returnProj=False, userNDV=None, band=None): def nodataToNan(inarr, listofvals) -> None: """Setting values to nan as needed.""" - inarr = inarr.astype(float) # nans cannot be integers (i.e. in DEM) + inarr = inarr.astype(float) # nans cannot be integers (i.e. in DEM) for val in listofvals: if val is not None: inarr[inarr == val] = np.nan @@ -223,10 +223,10 @@ def rio_stats(fname, band=1): fname = fname + '.vrt' # Turn off PAM to avoid creating .aux.xml files - with rasterio.Env(GDAL_PAM_ENABLED="NO"): + with rasterio.Env(GDAL_PAM_ENABLED='NO'): with rasterio.open(fname) as src: - gt = src.transform.to_gdal() - proj = src.crs + gt = src.transform.to_gdal() + proj = src.crs stats = src.statistics(band) return stats, proj, gt @@ -234,7 +234,7 @@ def rio_stats(fname, band=1): def get_file_and_band(filestr): """Support file;bandnum as input for filename strings.""" - parts = filestr.split(";") + parts = filestr.split(';') # Defaults to first band if no bandnum is provided if len(parts) == 1: @@ -242,20 +242,19 @@ def get_file_and_band(filestr): elif len(parts) == 2: return parts[0].strip(), int(parts[1].strip()) else: - raise ValueError( - f"Cannot interpret {filestr} as valid filename" - ) + raise ValueError(f'Cannot interpret {filestr} as valid filename') + -def writeArrayToRaster(array, filename, noDataValue=0., fmt='ENVI', proj=None, gt=None) -> None: +def writeArrayToRaster(array, filename, noDataValue=0.0, fmt='ENVI', proj=None, gt=None) -> None: """Write a numpy array to a GDAL-readable raster.""" array_shp = np.shape(array) if array.ndim != 2: raise RuntimeError(f'writeArrayToRaster: cannot write an array of shape {array_shp} to a raster image') # Data type - if "complex" in str(array.dtype): + if 'complex' in str(array.dtype): dtype = np.complex64 - elif "float" in str(array.dtype): + elif 'float' in str(array.dtype): dtype = np.float32 else: dtype = np.uint8 @@ -268,18 +267,25 @@ def writeArrayToRaster(array, filename, noDataValue=0., fmt='ENVI', proj=None, g except TypeError: trans = gt - ## cant write netcdfs with rasterio in a simple way + # cant write netcdfs with rasterio in a simple way if fmt == 'nc': fmt = 'GTiff' filename = filename.replace('.nc', '.tif') - with rasterio.open(filename, mode="w", count=1, - width=array_shp[1], height=array_shp[0], - dtype=dtype, crs=proj, nodata=noDataValue, - driver=fmt, transform=trans) as dst: + with rasterio.open( + filename, + mode='w', + count=1, + width=array_shp[1], + height=array_shp[0], + dtype=dtype, + crs=proj, + nodata=noDataValue, + driver=fmt, + transform=trans, + ) as dst: dst.write(array, 1) logger.info('Wrote: %s', filename) - return def round_date(date, precision): @@ -292,7 +298,7 @@ def round_date(date, precision): except TypeError: T0 = T0.replace(tzinfo=timezone(offset=timedelta())) datedelta = T0 - date - + # Round that timedelta to the specified precision rem = datedelta % precision # Add back to get date rounded up @@ -304,7 +310,7 @@ def round_date(date, precision): except TypeError: T0 = T0.replace(tzinfo=timezone(offset=timedelta())) datedelta = date - T0 - + rem = datedelta % precision round_down = date - rem @@ -337,7 +343,7 @@ def robmax(a): def _get_g_ll(lats): """Compute the variation in gravity constant with latitude.""" - return G1 * (1 - 0.002637 * cosd(2 * lats) + 0.0000059 * (cosd(2 * lats))**2) + return G1 * (1 - 0.002637 * cosd(2 * lats) + 0.0000059 * (cosd(2 * lats)) ** 2) def get_Re(lats): @@ -359,7 +365,7 @@ def get_Re(lats): >>> assert output[0] == 6378137 # (Rmax) >>> assert output[-1] == 6356752 # (Rmin) """ - return np.sqrt(1 / (((cosd(lats)**2) / Rmax**2) + ((sind(lats)**2) / Rmin**2))) + return np.sqrt(1 / (((cosd(lats) ** 2) / Rmax**2) + ((sind(lats) ** 2) / Rmin**2))) def geo_to_ht(lats, hts): @@ -385,8 +391,8 @@ def geo_to_ht(lats, hts): Returns: ndarray: geometric heights. These are approximate ellipsoidal heights referenced to WGS84 """ - g_ll = _get_g_ll(lats) # gravity function of latitude - Re = get_Re(lats) # Earth radius function of latitude + g_ll = _get_g_ll(lats) # gravity function of latitude + Re = get_Re(lats) # Earth radius function of latitude # Calculate Geometric Height, h h = (hts * Re) / (g_ll / g0 * Re - hts) @@ -412,9 +418,7 @@ def round_time(dt, roundTo=60): return dt + timedelta(0, rounding - seconds, -dt.microsecond) -def writeDelays(aoi, wetDelay, hydroDelay, - wetFilename, hydroFilename=None, - outformat=None, ndv=0.) -> None: +def writeDelays(aoi, wetDelay, hydroDelay, wetFilename, hydroFilename=None, outformat=None, ndv=0.0) -> None: """Write the delay numpy arrays to files in the format specified.""" if pd is None: raise ImportError('pandas is required to write GNSS delays to a file') @@ -425,7 +429,7 @@ def writeDelays(aoi, wetDelay, hydroDelay, # Do different things, depending on the type of input if aoi.type() == 'station_file': - df = pd.read_csv(aoi._filename).drop_duplicates(subset=["Lat", "Lon"]) + df = pd.read_csv(aoi._filename).drop_duplicates(subset=['Lat', 'Lon']) df['wetDelay'] = wetDelay df['hydroDelay'] = hydroDelay @@ -435,23 +439,9 @@ def writeDelays(aoi, wetDelay, hydroDelay, else: proj = aoi.projection() - gt = aoi.geotransform() - writeArrayToRaster( - wetDelay, - wetFilename, - noDataValue=ndv, - fmt=outformat, - proj=proj, - gt=gt - ) - writeArrayToRaster( - hydroDelay, - hydroFilename, - noDataValue=ndv, - fmt=outformat, - proj=proj, - gt=gt - ) + gt = aoi.geotransform() + writeArrayToRaster(wetDelay, wetFilename, noDataValue=ndv, fmt=outformat, proj=proj, gt=gt) + writeArrayToRaster(hydroDelay, hydroFilename, noDataValue=ndv, fmt=outformat, proj=proj, gt=gt) def getTimeFromFile(filename): @@ -462,7 +452,6 @@ def getTimeFromFile(filename): return datetime.strptime(out, fmt) - # Part of the following UTM and WGS84 converter is borrowed from https://gist.github.com/twpayne/4409500 # Credits go to Tom Payne @@ -554,7 +543,7 @@ def UTM_to_WGS84(z, l, x, y): return np.reshape(lon, shp), np.reshape(lat, shp) -def transform_bbox(snwe_in, dest_crs=4326, src_crs=4326, margin=100.): +def transform_bbox(snwe_in, dest_crs=4326, src_crs=4326, margin=100.0): """ Transform bbox to lat/lon or another CRS for use with rest of workflow. Returns: SNWE @@ -566,7 +555,7 @@ def transform_bbox(snwe_in, dest_crs=4326, src_crs=4326, margin=100.): src_crs = CRS(src_crs) # Handle margin for input bbox in degrees - if src_crs.axis_info[0].unit_name == "degree": + if src_crs.axis_info[0].unit_name == 'degree': margin = margin / 1.0e5 if isinstance(dest_crs, int): @@ -579,25 +568,26 @@ def transform_bbox(snwe_in, dest_crs=4326, src_crs=4326, margin=100.): return snwe_in T = Transformer.from_crs(src_crs, dest_crs, always_xy=True) - xs = np.linspace(snwe_in[2]-margin, snwe_in[3]+margin, num=11) - ys = np.linspace(snwe_in[0]-margin, snwe_in[1]+margin, num=11) + xs = np.linspace(snwe_in[2] - margin, snwe_in[3] + margin, num=11) + ys = np.linspace(snwe_in[0] - margin, snwe_in[1] + margin, num=11) X, Y = np.meshgrid(xs, ys) # Transform to lat/lon xx, yy = T.transform(X, Y) # query_area convention - snwe = [np.nanmin(yy), np.nanmax(yy), - np.nanmin(xx), np.nanmax(xx)] + snwe = [np.nanmin(yy), np.nanmax(yy), np.nanmin(xx), np.nanmax(xx)] return snwe def clip_bbox(bbox, spacing): """Clip box to multiple of spacing.""" - return [np.floor(bbox[0] / spacing) * spacing, - np.ceil(bbox[1] / spacing) * spacing, - np.floor(bbox[2] / spacing) * spacing, - np.ceil(bbox[3] / spacing) * spacing] + return [ + np.floor(bbox[0] / spacing) * spacing, + np.ceil(bbox[1] / spacing) * spacing, + np.floor(bbox[2] / spacing) * spacing, + np.ceil(bbox[3] / spacing) * spacing, + ] def requests_retry_session(retries=10, session=None): @@ -605,10 +595,12 @@ def requests_retry_session(retries=10, session=None): import requests from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry + # add a retry strategy; https://findwork.dev/blog/advanced-usage-python-requests-timeouts-retries-hooks/ session = session or requests.Session() - retry = Retry(total=retries, read=retries, connect=retries, - backoff_factor=0.3, status_forcelist=list(range(429, 505))) + retry = Retry( + total=retries, read=retries, connect=retries, backoff_factor=0.3, status_forcelist=list(range(429, 505)) + ) adapter = HTTPAdapter(max_retries=retry) session.mount('http://', adapter) session.mount('https://', adapter) @@ -616,16 +608,15 @@ def requests_retry_session(retries=10, session=None): def writeWeatherVarsXarray(lat, lon, h, q, p, t, dt, crs, outName=None, NoDataValue=-9999, chunk=(1, 91, 144)) -> None: - # I added datetime as an input to the function and just copied these two lines from merra2 for the attrs_dict attrs_dict = { - 'datetime': dt.strftime("%Y_%m_%dT%H_%M_%S"), - 'date_created': datetime.now().strftime("%Y_%m_%dT%H_%M_%S"), + 'datetime': dt.strftime('%Y_%m_%dT%H_%M_%S'), + 'date_created': datetime.now().strftime('%Y_%m_%dT%H_%M_%S'), 'NoDataValue': NoDataValue, 'chunksize': chunk, # 'mapping_name': mapping_name, } - + dimension_dict = { 'latitude': (('y', 'x'), lat), 'longitude': (('y', 'x'), lon), @@ -639,30 +630,30 @@ def writeWeatherVarsXarray(lat, lon, h, q, p, t, dt, crs, outName=None, NoDataVa } ds = xarray.Dataset( - data_vars=dataset_dict, - coords=dimension_dict, - attrs=attrs_dict, - ) - + data_vars=dataset_dict, + coords=dimension_dict, + attrs=attrs_dict, + ) + ds['h'].attrs['standard_name'] = 'mid_layer_heights' ds['p'].attrs['standard_name'] = 'mid_level_pressure' ds['q'].attrs['standard_name'] = 'specific_humidity' ds['t'].attrs['standard_name'] = 'air_temperature' - + ds['h'].attrs['units'] = 'm' ds['p'].attrs['units'] = 'Pa' ds['q'].attrs['units'] = 'kg kg-1' ds['t'].attrs['units'] = 'K' - ds["proj"] = 0 + ds['proj'] = 0 for k, v in crs.to_cf().items(): ds.proj.attrs[k] = v for var in ds.data_vars: ds[var].attrs['grid_mapping'] = 'proj' - + ds.to_netcdf(outName) del ds - + def convertLons(inLons): """Convert lons from 0-360 to -180-180.""" @@ -673,7 +664,6 @@ def convertLons(inLons): def read_NCMR_loginInfo(filepath=None): - from pathlib import Path if filepath is None: @@ -689,10 +679,9 @@ def read_NCMR_loginInfo(filepath=None): def read_EarthData_loginInfo(filepath=None): - from netrc import netrc - urs_usr, _, urs_pwd = netrc().hosts["urs.earthdata.nasa.gov"] + urs_usr, _, urs_pwd = netrc().hosts['urs.earthdata.nasa.gov'] return urs_usr, urs_pwd @@ -700,7 +689,7 @@ def show_progress(block_num, block_size, total_size) -> None: """Show download progress.""" if progressbar is None: raise ImportError('RAiDER.utilFcns: show_progress - progressbar is not available') - + global pbar if pbar is None: pbar = progressbar.ProgressBar(maxval=total_size) @@ -721,12 +710,7 @@ def getChunkSize(in_shape): minChunkSize = 100 maxChunkSize = 1000 cpu_count = mp.cpu_count() - chunkSize = tuple( - max( - min(maxChunkSize, s // cpu_count), - min(s, minChunkSize) - ) for s in in_shape - ) + chunkSize = tuple(max(min(maxChunkSize, s // cpu_count), min(s, minChunkSize)) for s in in_shape) return chunkSize @@ -763,15 +747,13 @@ def calcgeoh(lnsp, t, q, z, a, b, R_d, num_levels): if len(a) != num_levels + 1 or len(b) != num_levels + 1: raise ValueError( - f'I have here a model with {num_levels} levels, but parameters a ' + - f'and b have lengths {len(a)} and {len(b)} respectively. Of ' + - 'course, these three numbers should be equal.') + f'I have here a model with {num_levels} levels, but parameters a and b have lengths {len(a)} and {len(b)} ' + 'respectively. Of course, these three numbers should be equal.' + ) # Integrate up into the atmosphere from *lowest level* z_h = 0 # initial value - for lev, t_level, q_level in zip( - range(num_levels, 0, -1), t[::-1], q[::-1]): - + for lev, t_level, q_level in zip(range(num_levels, 0, -1), t[::-1], q[::-1]): # lev is the level number 1-60, we need a corresponding index # into ts and qs # ilevel = num_levels - lev # << this was Ray's original, but is a typo @@ -822,7 +804,7 @@ def transform_coords(proj1, proj2, x, y): def get_nearest_wmtimes(t0, time_delta): - """" + """ " Get the nearest two available times to the requested time given a time step. Args: @@ -841,7 +823,7 @@ def get_nearest_wmtimes(t0, time_delta): (datetime.datetime(2020, 1, 1, 9, 0), datetime.datetime(2020, 1, 1, 12, 0)) """ # get the closest time available - tclose = round_time(t0, roundTo = time_delta * 60 *60) + tclose = round_time(t0, roundTo=time_delta * 60 * 60) # Just calculate both options and take the closest t2_1 = tclose + timedelta(hours=time_delta) @@ -858,7 +840,7 @@ def get_nearest_wmtimes(t0, time_delta): return [t2, tclose] -def get_dt(t1,t2): +def get_dt(t1, t2): """ Helper function for getting the absolute difference in seconds between two python datetimes. @@ -876,5 +858,3 @@ def get_dt(t1,t2): 18000.0 """ return np.abs((t1 - t2).total_seconds()) - - From fe38b70dae100a9b39cd1adc7453d8dd59e923f3 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 17 Jul 2024 13:27:30 -0500 Subject: [PATCH 10/76] Add #672 to Unreleased --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21a32654a..ada78e36e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [PEP 440](https://www.python.org/dev/peps/pep-0440/) and uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +[Unreleased] +### Changed +* [672](https://github.com/dbekaert/RAiDER/pull/672) - Linted the project with `ruff`. + ## [0.5.2] ### Changed * [627](https://github.com/dbekaert/RAiDER/pull/627) - Made Python datetimes timezone-aware and added unit tests and bug fixes. From 934770dca438c31f8265413de6a68a0a971b6660 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 18 Jul 2024 16:37:21 -0500 Subject: [PATCH 11/76] Lint calcGUNW.py --- tools/RAiDER/aria/calcGUNW.py | 37 ++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/tools/RAiDER/aria/calcGUNW.py b/tools/RAiDER/aria/calcGUNW.py index 627528f56..dfb045327 100644 --- a/tools/RAiDER/aria/calcGUNW.py +++ b/tools/RAiDER/aria/calcGUNW.py @@ -1,15 +1,15 @@ """ -Calculate the interferometric phase from the 4 delays files of a GUNW and -write it to disk. +Calculate the interferometric phase from the 4 delays files of a GUNW and write it to disk. """ import os from datetime import datetime +from pathlib import Path import h5py +import netCDF4 import numpy as np import xarray as xr -from netCDF4 import Dataset import RAiDER from RAiDER.logger import logger @@ -23,7 +23,7 @@ DIM_NAMES = ['heightsMeta', 'latitudeMeta', 'longitudeMeta'] -def compute_delays_slc(cube_filenames: list, wavelength: float) -> xr.Dataset: +def compute_delays_slc(cube_paths: list[Path], wavelength: float) -> xr.Dataset: """ Get delays from standard RAiDER output formatting ouput including radian conversion and metadata. @@ -41,16 +41,16 @@ def compute_delays_slc(cube_filenames: list, wavelength: float) -> xr.Dataset: Formatted dataset for GUNW """ # parse date from filename - dct_delays = {} - for f in cube_filenames: - date = datetime.strptime(os.path.basename(f).split('_')[2], '%Y%m%dT%H%M%S') - dct_delays[date] = f + dct_delays: dict[datetime, Path] = {} + for path in cube_paths: + date = datetime.strptime(path.name.split('_')[2], '%Y%m%dT%H%M%S') + dct_delays[date] = path sec, ref = sorted(dct_delays.keys()) - wet_delays = [] - hyd_delays = [] - attrs_lst = [] + wet_delays: list[xr.DataArray] = [] + hyd_delays: list[xr.DataArray] = [] + attrs_lst: list[dict] = [] phase2range = (-4 * np.pi) / float(wavelength) for dt in [ref, sec]: path = dct_delays[dt] @@ -76,7 +76,7 @@ def compute_delays_slc(cube_filenames: list, wavelength: float) -> xr.Dataset: ds_slc[f'{key}_{TROPO_NAMES[0]}'] = wet_delays[i] ds_slc[f'{key}_{TROPO_NAMES[1]}'] = hyd_delays[i] - model = os.path.basename(path).split('_')[0] + model = path.name.split('_')[0] attrs = { 'units': 'radians', @@ -110,7 +110,8 @@ def compute_delays_slc(cube_filenames: list, wavelength: float) -> xr.Dataset: # first need to delete the variable; only can seem to with h5 -def update_gunw_slc(path_gunw: str, ds_slc) -> None: + +def update_gunw_slc(path_gunw: str, ds_slc: xr.Dataset) -> None: """Update the path_gunw file using the slc delays in ds_slc.""" with h5py.File(path_gunw, 'a') as h5: for k in TROPO_GROUP.split(): @@ -126,7 +127,7 @@ def update_gunw_slc(path_gunw: str, ds_slc) -> None: if k in h5.keys(): del h5[k] - with Dataset(path_gunw, mode='a') as ds: + with netCDF4.Dataset(path_gunw, mode='a') as ds: ds_grp = ds[TROPO_GROUP] ds_grp.createGroup(ds_slc.attrs['model'].upper()) ds_grp_wm = ds_grp[ds_slc.attrs['model'].upper()] @@ -174,13 +175,13 @@ def update_gunw_slc(path_gunw: str, ds_slc) -> None: logger.info('Updated %s group in: %s', os.path.basename(TROPO_GROUP), path_gunw) -def update_gunw_version(path_gunw) -> None: +def update_gunw_version(path_gunw: str) -> None: """Temporary hack for updating version to test aria-tools.""" - with Dataset(path_gunw, mode='a') as ds: + with netCDF4.Dataset(path_gunw, mode='a') as ds: ds.version = '1c' -def tropo_gunw_slc(cube_filenames: list, path_gunw: str, wavelength: float) -> xr.Dataset: +def tropo_gunw_slc(cube_paths: list[Path], path_gunw: str, wavelength: float) -> xr.Dataset: """ Compute and format the troposphere phase delay for GUNW from RAiDER outputs. @@ -198,7 +199,7 @@ def tropo_gunw_slc(cube_filenames: list, path_gunw: str, wavelength: float) -> x xr.Dataset Output cube that will be included in GUNW """ - ds_slc = compute_delays_slc(cube_filenames, wavelength) + ds_slc = compute_delays_slc(cube_paths, wavelength) # write the interferometric delay to disk update_gunw_slc(path_gunw, ds_slc) From d34846b7966ee05a697f2dc36780e2f414a66a13 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:41:49 -0500 Subject: [PATCH 12/76] Remove duplicate function test.__init__.py::update_yaml Two versions of update_yaml existed, the other of which was in tools.RAiDER.aria.prepFromGUNW.py. In order to keep the code clean I extracted the function out to utilFcns.py and redirected imports to point to it instead of the other versions. This function was also linted to use pathlib.Path. --- test/__init__.py | 34 ++----------------------------- test/test_datelist.py | 6 +++--- test/test_intersect.py | 6 +++--- test/test_slant.py | 6 +++--- test/test_synthetic.py | 10 ++++----- test/test_temporal_interpolate.py | 10 ++++----- tools/RAiDER/aria/prepFromGUNW.py | 29 ++------------------------ tools/RAiDER/utilFcns.py | 27 ++++++++++++++++++++++++ 8 files changed, 50 insertions(+), 78 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index 335d716bb..7d84435f2 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -7,6 +7,8 @@ import numpy as np import xarray as xr +from RAiDER.utilFcns import write_yaml + test_dir = Path(__file__).parents[0] TEST_DIR = test_dir.absolute() @@ -28,38 +30,6 @@ def pushd(dir): os.chdir(prevdir) -def update_yaml(dct_cfg:dict, dst:str='temp.yaml'): - """ Write a new yaml file from a dictionary. - - Updates parameters in the default 'template.yaml' file. - Each key:value pair will in 'dct_cfg' will overwrite that in the default - """ - import RAiDER - import yaml - - run_config_path = os.path.join( - os.path.dirname(RAiDER.__file__), - 'cli', - 'examples', - 'template', - 'template.yaml' - ) - - with open(run_config_path, 'r') as f: - try: - params = yaml.safe_load(f) - except yaml.YAMLError as exc: - print(exc) - raise ValueError(f'Something is wrong with the yaml file {run_config_path}') - - params = {**params, **dct_cfg} - - with open(dst, 'w') as fh: - yaml.safe_dump(params, fh, default_flow_style=False) - - return dst - - def makeLatLonGrid(bbox, reg, out_dir, spacing=0.1): """ Make lat lons at a specified spacing """ S, N, W, E = bbox diff --git a/test/test_datelist.py b/test/test_datelist.py index 59166faac..1201863d7 100644 --- a/test/test_datelist.py +++ b/test/test_datelist.py @@ -1,7 +1,7 @@ import datetime import os import shutil -from test import TEST_DIR, WM, update_yaml +from test import TEST_DIR, WM, write_yaml from RAiDER.cli.raider import read_run_config_file def test_datelist(): @@ -26,7 +26,7 @@ def test_datelist(): } } - cfg = update_yaml(dct_group, 'temp.yaml') + cfg = write_yaml(dct_group, 'temp.yaml') param_dict = read_run_config_file(cfg) assert param_dict['date_list'] == true_dates @@ -51,6 +51,6 @@ def test_datestep(): } } - cfg = update_yaml(dct_group, 'temp.yaml') + cfg = write_yaml(dct_group, 'temp.yaml') param_dict = read_run_config_file(cfg) assert param_dict['date_list'] == true_dates \ No newline at end of file diff --git a/test/test_intersect.py b/test/test_intersect.py index 3dcccf5b1..416b24a09 100644 --- a/test/test_intersect.py +++ b/test/test_intersect.py @@ -7,7 +7,7 @@ from scipy.interpolate import griddata import rasterio -from test import TEST_DIR, WM_DIR, update_yaml +from test import TEST_DIR, WM_DIR, write_yaml @pytest.mark.parametrize('wm', 'ERA5'.split()) @@ -39,7 +39,7 @@ def test_cube_intersect(wm): } ## generate the default run config file and overwrite it with new parms - cfg = update_yaml(grp, 'temp.yaml') + cfg = write_yaml(grp, 'temp.yaml') ## run raider and intersect cmd = f'raider.py {cfg}' @@ -85,7 +85,7 @@ def test_gnss_intersect(wm): } ## generate the default run config file and overwrite it with new parms - cfg = update_yaml(grp) + cfg = write_yaml(grp, 'temp.yaml') ## run raider and intersect cmd = f'raider.py {cfg}' diff --git a/test/test_slant.py b/test/test_slant.py index 598d2f2e1..b5e51104b 100644 --- a/test/test_slant.py +++ b/test/test_slant.py @@ -8,7 +8,7 @@ import xarray as xr from test import ( - TEST_DIR, WM_DIR, ORB_DIR, update_yaml, make_delay_name + TEST_DIR, WM_DIR, ORB_DIR, write_yaml, make_delay_name ) @pytest.mark.parametrize('weather_model_name', ['ERA5']) @@ -39,7 +39,7 @@ def test_slant_proj(weather_model_name): } ## generate the default run config file and overwrite it with new parms - cfg = update_yaml(grp, 'temp.yaml') + cfg = write_yaml(grp, 'temp.yaml') ## run raider and intersect cmd = f'raider.py {cfg}' @@ -90,7 +90,7 @@ def test_ray_tracing(weather_model_name): } ## generate the default run config file and overwrite it with new parms - cfg = update_yaml(grp, 'temp.yaml') + cfg = write_yaml(grp, 'temp.yaml') ## run raider and intersect cmd = f'raider.py {cfg}' diff --git a/test/test_synthetic.py b/test/test_synthetic.py index 0ac1582e1..885161611 100644 --- a/test/test_synthetic.py +++ b/test/test_synthetic.py @@ -16,7 +16,7 @@ from RAiDER.cli.validators import modelName2Module from test import ( - TEST_DIR, ORB_DIR, WM_DIR, update_yaml + TEST_DIR, ORB_DIR, WM_DIR, write_yaml ) @@ -193,7 +193,7 @@ def test_dl_real(region, mod='ERA5'): op.dirname(SAobj.path_wm_real) dct_cfg['download_only'] = True - cfg = update_yaml(dct_cfg) + cfg = write_yaml(dct_cfg, 'temp.yaml') ## run raider to download the real weather model cmd = f'raider.py {cfg}' @@ -228,7 +228,7 @@ def test_hydrostatic_eq(region, mod='ERA-5'): ## update the weather model; t = p for hydrostatic path_synth = update_model(SAobj.path_wm_real, 'hydro', SAobj.wm_dir_synth) - cfg = update_yaml(dct_cfg) + cfg = write_yaml(dct_cfg, 'temp.yaml') ## run raider with the synthetic model cmd = f'raider.py {cfg}' @@ -297,7 +297,7 @@ def test_wet_eq_linear(region, mod='ERA-5'): ## update the weather model; t = e for wet1 path_synth = update_model(SAobj.path_wm_real, 'wet_linear', SAobj.wm_dir_synth) - cfg = update_yaml(dct_cfg) + cfg = write_yaml(dct_cfg, 'temp.yaml') ## run raider with the synthetic model cmd = f'raider.py {cfg}' @@ -364,7 +364,7 @@ def test_wet_eq_nonlinear(region, mod='ERA-5'): ## update the weather model; t = e for wet1 path_synth = update_model(SAobj.path_wm_real, 'wet_nonlinear', SAobj.wm_dir_synth) - cfg = update_yaml(dct_cfg) + cfg = write_yaml(dct_cfg, 'temp.yaml') ## run raider with the synthetic model cmd = f'raider.py {cfg}' diff --git a/test/test_temporal_interpolate.py b/test/test_temporal_interpolate.py index 1bd3fdee3..488195939 100644 --- a/test/test_temporal_interpolate.py +++ b/test/test_temporal_interpolate.py @@ -8,7 +8,7 @@ from test import ( - WM, TEST_DIR, update_yaml + WM, TEST_DIR, write_yaml ) from RAiDER.logger import logger @@ -40,7 +40,7 @@ def test_cube_timemean(): for hr in [hr1, hr2]: grp['time_group'].update({'time': f'{hr}:00:00'}) ## generate the default run config file and overwrite it with new parms - cfg = update_yaml(grp) + cfg = write_yaml(grp, 'temp.yaml') ## run raider for the default date cmd = f'raider.py {cfg}' @@ -49,7 +49,7 @@ def test_cube_timemean(): ## run interpolation in the middle of the two grp['time_group'] = {'time': ti, 'interpolate_time': 'center_time'} - cfg = update_yaml(grp) + cfg = write_yaml(grp, 'temp.yaml') cmd = f'raider.py {cfg}' proc = subprocess.run(cmd.split(), stdout=subprocess.PIPE, universal_newlines=True) @@ -102,7 +102,7 @@ def test_cube_weighting(): for hr in [hr1, hr2]: grp['time_group'].update({'time': f'{hr}:00:00'}) ## generate the default run config file and overwrite it with new parms - cfg = update_yaml(grp) + cfg = write_yaml(grp, 'temp.yaml') ## run raider for the default date cmd = f'raider.py {cfg}' @@ -111,7 +111,7 @@ def test_cube_weighting(): ## run interpolation very near the first grp['time_group'] = {'time': ti, 'interpolate_time': 'center_time'} - cfg = update_yaml(grp) + cfg = write_yaml(grp, 'temp.yaml') cmd = f'raider.py {cfg}' proc = subprocess.run(cmd.split(), stdout=subprocess.PIPE, universal_newlines=True) diff --git a/tools/RAiDER/aria/prepFromGUNW.py b/tools/RAiDER/aria/prepFromGUNW.py index 6021816d6..ad0522a7d 100644 --- a/tools/RAiDER/aria/prepFromGUNW.py +++ b/tools/RAiDER/aria/prepFromGUNW.py @@ -349,31 +349,6 @@ def make_cube(self): logger.info('Wrote cube to: %s', dst_cube) return dst_cube - -def update_yaml(dct_cfg: dict, dst: str = 'GUNW.yaml'): - """Write a new yaml file from a dictionary. - - Updates parameters in the default 'template.yaml' file. - Each key:value pair will in 'dct_cfg' will overwrite that in the default - """ - run_config_path = os.path.join(os.path.dirname(RAiDER.__file__), 'cli', 'examples', 'template', 'template.yaml') - - with open(run_config_path) as f: - try: - params = yaml.safe_load(f) - except yaml.YAMLError as exc: - print(exc) - raise ValueError(f'Something is wrong with the yaml file {run_config_path}') - - params = {**params, **dct_cfg} - - with open(dst, 'w') as fh: - yaml.safe_dump(params, fh, default_flow_style=False) - - logger.info('Wrote new cfg file: %s', dst) - return dst - - def main(args): """Read parameters needed for RAiDER from ARIA Standard Products (GUNW).""" # Check if WEATHER MODEL API credentials hidden file exists, if not create it or raise ERROR @@ -404,6 +379,6 @@ def main(args): }, } - path_cfg = f'GUNW_{GUNWObj.name}.yaml' - update_yaml(raider_cfg, path_cfg) + path_cfg = Path(f'GUNW_{GUNWObj.name}.yaml') + write_yaml(raider_cfg, path_cfg) return path_cfg, GUNWObj.wavelength diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 5ef280004..ae912ab9f 100755 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -1,13 +1,17 @@ """Geodesy-related utility functions.""" import os +from pathlib import Path import re from datetime import datetime, timedelta, timezone +from typing import Any +import RAiDER import numpy as np import xarray from numpy import ndarray from pyproj import CRS, Proj, Transformer +import yaml # Optional imports @@ -858,3 +862,26 @@ def get_dt(t1, t2): 18000.0 """ return np.abs((t1 - t2).total_seconds()) + + +def write_yaml(content: dict[Any, Any], dst: Path) -> Path: + """Write a new yaml file from a dictionary with template.yaml as a base. + + Each key-value pair in 'content' will override the one from template.yaml. + """ + yaml_path = Path(RAiDER.__file__).parent / 'cli/examples/template/template.yaml' + + with yaml_path.open() as f: + try: + params = yaml.safe_load(f) + except yaml.YAMLError as exc: + print(exc) + raise ValueError(f'Something is wrong with the yaml file {yaml_path}') + + params = {**params, **content} + + with dst.open('w') as fh: + yaml.safe_dump(params, fh, default_flow_style=False) + + logger.info('Wrote new cfg file: %s', str(dst)) + return dst From a0a9d3c95b96c98fb79502d6ff345e4469e60e46 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 18 Jul 2024 20:42:27 -0500 Subject: [PATCH 13/76] Lint prepFromGUNW.py Missing docstrings (D100, D101, D102, D103, D107) ignored for now. Also skipped linting on the GUNW methods that are marked unused. As part of the linting, all functions and methods were given type annotations. This ended up reaching into a few other files, most notably including the GUNW workflow's argument parsing. --- tools/RAiDER/aria/prepFromGUNW.py | 112 ++++++++++++++++-------------- tools/RAiDER/cli/__main__.py | 3 +- tools/RAiDER/cli/conf.py | 7 +- tools/RAiDER/cli/raider.py | 83 +++++++++++----------- tools/RAiDER/logger.py | 7 +- tools/RAiDER/types.py | 36 ++++++++++ 6 files changed, 149 insertions(+), 99 deletions(-) create mode 100644 tools/RAiDER/types.py diff --git a/tools/RAiDER/aria/prepFromGUNW.py b/tools/RAiDER/aria/prepFromGUNW.py index ad0522a7d..c67fbded7 100644 --- a/tools/RAiDER/aria/prepFromGUNW.py +++ b/tools/RAiDER/aria/prepFromGUNW.py @@ -5,26 +5,24 @@ # RESERVED. United States Government Sponsorship acknowledged. # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -import os import sys -from dataclasses import dataclass from datetime import datetime, timedelta, timezone -from typing import Literal +from pathlib import Path import numpy as np import pandas as pd import rasterio import shapely.wkt import xarray as xr -import yaml from shapely.geometry import box -import RAiDER from RAiDER.logger import logger from RAiDER.models import credentials from RAiDER.models.hrrr import AK_GEO, HRRR_CONUS_COVERAGE_POLYGON, check_hrrr_dataset_availability from RAiDER.s1_azimuth_timing import get_times_for_azimuth_interpolation from RAiDER.s1_orbits import get_orbits_from_slc_ids_hyp3lib +from RAiDER.types import BB, CalcDelaysArgs, LookDir +from RAiDER.utilFcns import write_yaml # cube spacing in degrees for each model @@ -156,63 +154,70 @@ def check_weather_model_availability(gunw_path: str, weather_model_name: str) -> return ref_cond and sec_cond -@dataclass class GUNW: - path_gunw: str - wm: str - out_dir: str + path_gunw: Path + wm: str # TODO(garlic-os): probably a known weather model name + out_dir: Path + SNWE: BB.SNWE + heights: list[int] + dates: list[int] # ints in YYYYMMDD form + mid_time: str # str in HH:MM:SS form + look_dir: LookDir + wavelength: float + name: str + orbit_file: ... + spacing_m: int + + def __init__(self, path_gunw: str, wm: str, out_dir: str) -> None: + self.path_gunw = Path(path_gunw) + self.wm = wm + self.out_dir = Path(out_dir) - def __post_init__(self): self.SNWE = self.get_bbox() self.heights = np.arange(-500, 9500, 500).tolist() # self.heights = [-500, 0] self.dates, self.mid_time = self.get_datetimes() - self.look_dir = self.get_look_dir() self.wavelength = self.get_wavelength() self.name = self.make_fname() - self.OrbitFile = self.get_orbit_file() + self.orbit_file = self.get_orbit_file() self.spacing_m = int(DCT_POSTING[self.wm] * 1e5) - # not implemented # self.spacing_m = self.calc_spacing_UTM() # probably wrong/unnecessary # self.lat_file, self.lon_file = self.makeLatLonGrid_native() # self.path_cube = self.make_cube() # not needed - def get_bbox(self): + def get_bbox(self) -> BB.SNWE: """Get the bounding box (SNWE) from an ARIA GUNW product.""" with xr.open_dataset(self.path_gunw) as ds: poly_str = ds['productBoundingBox'].data[0].decode('utf-8') - poly = shapely.wkt.loads(poly_str) W, S, E, N = poly.bounds - - return [S, N, W, E] + return S, N, W, E def make_fname(self) -> str: """Match the ref/sec filename (SLC dates may be different around edge cases).""" - ref, sec = os.path.basename(self.path_gunw).split('-')[6].split('_') - mid_time = os.path.basename(self.path_gunw).split('-')[7] + ref, sec = self.path_gunw.name.split('-')[6].split('_') + mid_time = self.path_gunw.name.split('-')[7] return f'{ref}-{sec}_{mid_time}' - def get_datetimes(self): + def get_datetimes(self) -> tuple[list[int], str]: """Get the datetimes and set the satellite for orbit.""" ref_sec = self.get_slc_dt() - middates = [] - for aq in ref_sec: - st, en = aq - midpt = st + (en - st) / 2 - middates.append(int(midpt.date().strftime('%Y%m%d'))) - midtime = midpt.time().strftime('%H:%M:%S') - return middates, midtime - - def get_slc_dt(self): + mid_dates: list[int] = [] # dates in YYYYMMDD format + for st, en in ref_sec: + midpoint = st + (en - st) / 2 + mid_dates.append(int(midpoint.date().strftime('%Y%m%d'))) + mid_time = midpoint.time().strftime('%H:%M:%S') + return mid_dates, mid_time + + def get_slc_dt(self) -> list[tuple[datetime, datetime]]: """Grab the SLC start date and time from the GUNW.""" group = 'science/radarMetaData/inputSLC' - lst_sten = [] + lst_sten: list[tuple[datetime, datetime]] = [] for key in 'reference secondary'.split(): - ds = xr.open_dataset(self.path_gunw, group=f'{group}/{key}') - slcs = ds['L1InputGranules'] + with xr.open_dataset(self.path_gunw, group=f'{group}/{key}') as ds: + slcs = ds['L1InputGranules'] nslcs = slcs.count().item() # single slc if nslcs == 1: @@ -241,12 +246,12 @@ def get_slc_dt(self): assert st > datetime(1989, 3, 1), \ f'Missing {key} SLC metadata in GUNW: {self.f}' - lst_sten.append([st, en]) + lst_sten.append((st, en)) return lst_sten - def get_look_dir(self) -> Literal['right', 'left']: - look_dir = os.path.basename(self.path_gunw).split('-')[3].lower() + def get_look_dir(self) -> LookDir: + look_dir = self.path_gunw.name.split('-')[3].lower() return 'right' if look_dir == 'r' else 'left' def get_wavelength(self): @@ -255,19 +260,20 @@ def get_wavelength(self): wavelength = ds['wavelength'].item() return wavelength - def get_orbit_file(self): + # TODO(garlic-os): sounds like this returns one thing but it returns a list? + def get_orbit_file(self) -> list[str]: """Get orbit file for reference (GUNW: first & later date).""" - orbit_dir = os.path.join(self.out_dir, 'orbits') - os.makedirs(orbit_dir, exist_ok=True) + orbit_dir = self.out_dir / 'orbits' + orbit_dir.mkdir(parents=True, exist_ok=True) # just to get the correct satellite group = 'science/radarMetaData/inputSLC/reference' - ds = xr.open_dataset(self.path_gunw, group=f'{group}') - slcs = ds['L1InputGranules'] + with xr.open_dataset(self.path_gunw, group=f'{group}') as ds: + slcs = ds['L1InputGranules'] # Convert to list of strings slcs_lst = [slc for slc in slcs.data.tolist() if slc] - # Remove .zip from the granule ids included in this field + # Remove ".zip" from the granule ids included in this field slcs_lst = list(map(lambda slc: slc.replace('.zip', ''), slcs_lst)) path_orb = get_orbits_from_slc_ids_hyp3lib(slcs_lst) @@ -306,7 +312,7 @@ def calc_spacing_UTM(self, posting: float = 0.01): lat_spacing_m = np.subtract(*res[3][::-1]) return np.mean([lon_spacing_m, lat_spacing_m]) - def makeLatLonGrid_native(self): + def makeLatLonGrid_native(self) -> tuple[Path, Path]: """Make LatLonGrid at GUNW spacing (90m = 0.00083333º).""" group = 'science/grids/data' with xr.open_dataset(self.path_gunw, group=group) as ds0: @@ -319,8 +325,8 @@ def makeLatLonGrid_native(self): da_lon = xr.DataArray(Lon.T, coords=[Lon[0, :], Lat[:, 0]], dims=dims) da_lat = xr.DataArray(Lat.T, coords=[Lon[0, :], Lat[:, 0]], dims=dims) - dst_lat = os.path.join(self.out_dir, 'latitude.geo') - dst_lon = os.path.join(self.out_dir, 'longitude.geo') + dst_lat = self.out_dir / 'latitude.geo' + dst_lon = self.out_dir / 'longitude.geo' da_lat.to_netcdf(dst_lat) da_lon.to_netcdf(dst_lon) @@ -329,7 +335,7 @@ def makeLatLonGrid_native(self): logger.debug('Wrote: %s', dst_lon) return dst_lat, dst_lon - def make_cube(self): + def make_cube(self) -> Path: """Make LatLonGrid at GUNW spacing (90m = 0.00083333º).""" group = 'science/grids/data' with xr.open_dataset(self.path_gunw, group=group) as ds0: @@ -339,17 +345,17 @@ def make_cube(self): lat_st, lat_en = np.floor(lats0.min()), np.ceil(lats0.max()) lon_st, lon_en = np.floor(lons0.min()), np.ceil(lons0.max()) - lats = np.arange(lat_st, lat_en, DCT_POSTING[self.wmodel]) - lons = np.arange(lon_st, lon_en, DCT_POSTING[self.wmodel]) + lats = np.arange(lat_st, lat_en, DCT_POSTING[self.wm]) + lons = np.arange(lon_st, lon_en, DCT_POSTING[self.wm]) - ds = xr.Dataset(coords={'latitude': lats, 'longitude': lons, 'heights': self.heights}) - dst_cube = os.path.join(self.out_dir, f'GeoCube_{self.name}.nc') - ds.to_netcdf(dst_cube) + dst_cube = self.out_dir / f'GeoCube_{self.name}.nc' + with xr.Dataset(coords={'latitude': lats, 'longitude': lons, 'heights': self.heights}) as ds: + ds.to_netcdf(dst_cube) - logger.info('Wrote cube to: %s', dst_cube) + logger.info('Wrote cube to: %s', str(dst_cube)) return dst_cube -def main(args): +def main(args: CalcDelaysArgs) -> tuple[Path, float]: """Read parameters needed for RAiDER from ARIA Standard Products (GUNW).""" # Check if WEATHER MODEL API credentials hidden file exists, if not create it or raise ERROR credentials.check_api(args.weather_model, args.api_uid, args.api_key) @@ -370,7 +376,7 @@ def main(args): }, 'los_group': { 'ray_trace': True, - 'orbit_file': GUNWObj.OrbitFile, + 'orbit_file': GUNWObj.orbit_file, 'wavelength': GUNWObj.wavelength, }, 'runtime_group': { diff --git a/tools/RAiDER/cli/__main__.py b/tools/RAiDER/cli/__main__.py index 7756cf424..24c01c101 100644 --- a/tools/RAiDER/cli/__main__.py +++ b/tools/RAiDER/cli/__main__.py @@ -1,6 +1,7 @@ import argparse import sys from importlib.metadata import entry_points +from pathlib import Path import RAiDER.cli.conf as conf @@ -23,7 +24,7 @@ def main() -> None: args, unknowns = parser.parse_known_args() # Needed for a global logging path - conf.setLoggerPath(args.logger_path) + conf.setLoggerPath(Path(args.logger_path)) sys.argv = [args.process, *unknowns] diff --git a/tools/RAiDER/cli/conf.py b/tools/RAiDER/cli/conf.py index 73d5e7356..4d8167b05 100644 --- a/tools/RAiDER/cli/conf.py +++ b/tools/RAiDER/cli/conf.py @@ -1,6 +1,9 @@ -LOGGER_PATH = None +from typing import Optional +from pathlib import Path +LOGGER_PATH: Optional[Path] = None -def setLoggerPath(path) -> None: + +def setLoggerPath(path: Path) -> None: global LOGGER_PATH LOGGER_PATH = path diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 4f29b2925..a4c6e94a6 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -6,7 +6,9 @@ import sys from pathlib import Path from textwrap import dedent +from typing import Literal, Optional, cast +from RAiDER.types import CalcDelaysArgs, CalcDelaysArgsUnvalidated, TimeInterpolationMethod import numpy as np import xarray as xr import yaml @@ -136,7 +138,7 @@ def drop_nans(d): return d -def calcDelays(iargs=None): +def calcDelays(iargs=None) -> list[Path]: """Parse command line arguments using argparse.""" import RAiDER import RAiDER.processWM @@ -244,7 +246,7 @@ def calcDelays(iargs=None): model.set_latlon_bounds(wm_bounds, output_spacing=aoi.get_output_spacing()) - wet_filenames = [] + wet_paths: list[Path] = [] for t, w, f in zip(params['date_list'], params['wetFilenames'], params['hydroFilenames']): ########################################################### # Weather model calculation @@ -357,9 +359,9 @@ def calcDelays(iargs=None): if aoi.type() in ['station_file', 'radar_rasters', 'geocoded_file']: writeDelays(aoi, wet_delay, hydro_delay, out_filename, f, outformat=params['raster_format']) - wet_filenames.append(out_filename) + wet_paths.append(Path(out_filename)) - return wet_filenames + return wet_paths # ------------------------------------------------------ downloadGNSSDelays.py @@ -471,7 +473,7 @@ def downloadGNSS() -> None: # ------------------------------------------------------------ prepFromGUNW.py -def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: +def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> xr.Dataset: p = argparse.ArgumentParser( description='Calculate a cube of interferometic delays for GUNW files', formatter_class=argparse.ArgumentDefaultsHelpFormatter, @@ -499,7 +501,6 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: p.add_argument( '-f', '--file', - type=str, help='1 ARIA GUNW netcdf file' ) @@ -507,7 +508,6 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: '-m', '--weather-model', default='HRRR', - type=str, choices=['None'] + ALLOWED_MODELS, help='Weather model.' ) @@ -516,7 +516,6 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: '-uid', '--api_uid', default=None, - type=str, help='Weather model API UID [uid, email, username], depending on model.', ) @@ -524,7 +523,6 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: '-key', '--api_key', default=None, - type=str, help='Weather model API KEY [key, password], depending on model.' ) @@ -532,7 +530,6 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: '-interp', '--interpolate-time', default='azimuth_time_grid', - type=str, choices=TIME_INTERPOLATION_METHODS, help=( 'How to interpolate across model time steps. Possible options are: ' @@ -545,19 +542,20 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: p.add_argument( '-o', '--output-directory', - default=os.getcwd(), - type=str, help='Directory to store results.' + default=str(Path.cwd()), + help='Directory to store results.' ) - iargs = p.parse_args(iargs) + args: CalcDelaysArgsUnvalidated = p.parse_args(iargs, namespace=CalcDelaysArgsUnvalidated()) - if not iargs.input_bucket_prefix: - iargs.input_bucket_prefix = iargs.bucket_prefix + if args.input_bucket_prefix is None: + args.input_bucket_prefix = args.bucket_prefix - if iargs.interpolate_time not in ['none', 'center_time', 'azimuth_time_grid']: + if args.interpolate_time not in TIME_INTERPOLATION_METHODS: raise ValueError("interpolate_time arg must be in ['none', 'center_time', 'azimuth_time_grid']") + args.interpolate_time = cast(TimeInterpolationMethod, args.interpolate_time) - if iargs.weather_model == 'None': + if args.weather_model == 'None': # NOTE: HyP3's current step function implementation does not have a good way of conditionally # running processing steps. This allows HyP3 to always run this step but exit immediately # and do nothing if tropospheric correction via RAiDER is not selected. This should not cause @@ -565,22 +563,28 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: print('Nothing to do!') return - if iargs.file and (iargs.weather_model == 'HRRR') and (iargs.interpolate_time == 'azimuth_time_grid'): - file_name = iargs.file.split('/')[-1] + if ( + args.file is not None and + args.weather_model == 'HRRR' and + args.interpolate_time == 'azimuth_time_grid' + ): + file_name = args.file.split('/')[-1] gunw_id = file_name.replace('.nc', '') if not RAiDER.aria.prepFromGUNW.check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id): raise NoWeatherModelData('The required HRRR data for time-grid interpolation is not available') - if not iargs.file and iargs.bucket: + if args.file is None: + if args.bucket is None: + raise ValueError('Either argument --file or --bucket must be provided') + # only use GUNW ID for checking if HRRR available - iargs.file = aws.get_s3_file(iargs.bucket, iargs.input_bucket_prefix, '.nc') - if iargs.file is None: + args.file = aws.get_s3_file(args.bucket, args.input_bucket_prefix, '.nc') + if args.file is None: raise ValueError( - 'GUNW product file could not be found at' f's3://{iargs.bucket}/{iargs.input_bucket_prefix}' + 'GUNW product file could not be found at' f's3://{args.bucket}/{args.input_bucket_prefix}' ) - if iargs.weather_model == 'HRRR' and (iargs.interpolate_time == 'azimuth_time_grid'): - file_name_str = str(iargs.file) - gunw_nc_name = file_name_str.split('/')[-1] + if args.weather_model == 'HRRR' and args.interpolate_time == 'azimuth_time_grid': + gunw_nc_name = args.file.split('/')[-1] gunw_id = gunw_nc_name.replace('.nc', '') if not RAiDER.aria.prepFromGUNW.check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id): print( @@ -589,33 +593,32 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: return # Download file to obtain metadata - if not RAiDER.aria.prepFromGUNW.check_weather_model_availability(iargs.file, iargs.weather_model): + if not RAiDER.aria.prepFromGUNW.check_weather_model_availability(args.file, args.weather_model): # NOTE: We want to submit jobs that are outside of acceptable weather model range # and still deliver these products to the DAAC without this layer. Therefore # we include this within this portion of the control flow. print('Nothing to do because outside of weather model range') return - json_file_path = aws.get_s3_file(iargs.bucket, iargs.input_bucket_prefix, '.json') + json_file_path = aws.get_s3_file(args.bucket, args.input_bucket_prefix, '.json') if json_file_path is None: raise ValueError( - 'GUNW metadata file could not be found at' f's3://{iargs.bucket}/{iargs.input_bucket_prefix}' + 'GUNW metadata file could not be found at' f's3://{args.bucket}/{args.input_bucket_prefix}' ) json_data = json.load(open(json_file_path)) - json_data['metadata'].setdefault('weather_model', []).append(iargs.weather_model) + json_data['metadata'].setdefault('weather_model', []).append(args.weather_model) json.dump(json_data, open(json_file_path, 'w')) # also get browse image -- if RAiDER is running in its own HyP3 job, the browse image will be needed for ingest - browse_file_path = aws.get_s3_file(iargs.bucket, iargs.input_bucket_prefix, '.png') + browse_file_path = aws.get_s3_file(args.bucket, args.input_bucket_prefix, '.png') if browse_file_path is None: raise ValueError( - 'GUNW browse image could not be found at' f's3://{iargs.bucket}/{iargs.input_bucket_prefix}' + 'GUNW browse image could not be found at' f's3://{args.bucket}/{args.input_bucket_prefix}' ) - elif not iargs.file: - raise ValueError('Either argument --file or --bucket must be provided') + args = cast(CalcDelaysArgs, args) # prep the config needed for delay calcs - path_cfg, wavelength = RAiDER.aria.prepFromGUNW.main(iargs) + path_cfg, wavelength = RAiDER.aria.prepFromGUNW.main(args) # write delay cube (nc) to disk using config # return a list with the path to cube for each date @@ -626,15 +629,15 @@ def calcDelaysGUNW(iargs: list[str] = None) -> xr.Dataset: # calculate the interferometric phase and write it out ds = RAiDER.aria.calcGUNW.tropo_gunw_slc( cube_filenames, - iargs.file, + args.file, wavelength, ) # upload to s3 - if iargs.bucket: - aws.upload_file_to_s3(iargs.file, iargs.bucket, iargs.bucket_prefix) - aws.upload_file_to_s3(json_file_path, iargs.bucket, iargs.bucket_prefix) - aws.upload_file_to_s3(browse_file_path, iargs.bucket, iargs.bucket_prefix) + if args.bucket is not None: + aws.upload_file_to_s3(args.file, args.bucket, args.bucket_prefix) + aws.upload_file_to_s3(json_file_path, args.bucket, args.bucket_prefix) + aws.upload_file_to_s3(browse_file_path, args.bucket, args.bucket_prefix) return ds diff --git a/tools/RAiDER/logger.py b/tools/RAiDER/logger.py index 3f5b36a9b..483b58b0a 100644 --- a/tools/RAiDER/logger.py +++ b/tools/RAiDER/logger.py @@ -9,6 +9,7 @@ import logging import os +from pathlib import Path import sys from logging import FileHandler, Formatter, StreamHandler @@ -53,7 +54,7 @@ def formatMessage(self, record): ##################################### # DEFINE THE LOGGER if conf.LOGGER_PATH is None: - logger_path = os.getcwd() + logger_path = Path.cwd() else: logger_path = conf.LOGGER_PATH @@ -64,13 +65,13 @@ def formatMessage(self, record): stdout_handler.setFormatter(CustomFormatter(use_color=os.name != 'nt')) stdout_handler.setLevel(logging.DEBUG) -debugfile_handler = FileHandler(os.path.join(logger_path, 'debug.log')) +debugfile_handler = FileHandler(logger_path / 'debug.log') debugfile_handler.setFormatter( Formatter('[{asctime}] {levelname:<10} {module} {exc_info} {funcName:>20}:{lineno:<5} {message}', style='{') ) debugfile_handler.setLevel(logging.DEBUG) -errorfile_handler = FileHandler(os.path.join(logger_path, 'error.log')) +errorfile_handler = FileHandler(logger_path / 'error.log') errorfile_handler.setFormatter( Formatter('[{asctime}] {levelname:<10} {module:<10} {exc_info} {funcName:>20}:{lineno:<5} {message}', style='{') ) diff --git a/tools/RAiDER/types.py b/tools/RAiDER/types.py new file mode 100644 index 000000000..d16ed3126 --- /dev/null +++ b/tools/RAiDER/types.py @@ -0,0 +1,36 @@ +"""Types specific to RAiDER.""" + +import argparse +from dataclasses import dataclass +from typing import Literal, Optional + + +@dataclass +class BB: + SNWE = tuple[float, float, float, float] + +LookDir = Literal['right', 'left'] + +TimeInterpolationMethod = Literal['none', 'center_time', 'azimuth_time_grid'] + +class CalcDelaysArgsUnvalidated(argparse.Namespace): + bucket: Optional[str] + bucket_prefix: Optional[str] + input_bucket_prefix: Optional[str] + file: Optional[str] + weather_model: str + api_uid: Optional[str] + api_key: Optional[str] + interpolate_time: str + output_directory: str + +class CalcDelaysArgs(CalcDelaysArgsUnvalidated): + bucket: Optional[str] + bucket_prefix: Optional[str] + input_bucket_prefix: Optional[str] + file: str + weather_model: str + api_uid: Optional[str] + api_key: Optional[str] + interpolate_time: TimeInterpolationMethod + output_directory: str \ No newline at end of file From 085146c08e3974e1956c1393b5fb4cfb48183e6a Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 18 Jul 2024 20:45:47 -0500 Subject: [PATCH 14/76] ruff: ignore ANN101, D200, and D205 ANN101 is adding the type annotation "Self" to every method's self argument. This rule is controversial and in my opinion would be of little benefit for this project. D200 and D205 are both about the exact formatting of docstrings. I am electing not to make any fixes for these rules since it would involve revising so documentation. This could be revisited in the future with project experts' input. --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9b1979312..8ac601856 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,6 @@ default_section = "THIRDPARTY" [tool.setuptools_scm] - [tool.ruff] line-length = 120 src = ["tools", "test"] @@ -84,7 +83,7 @@ extend-select = [ "ANN", # annotations: https://docs.astral.sh/ruff/rules/#flake8-annotations-ann "PTH", # use-pathlib-pth: https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth ] -ignore = ["D212"] +ignore = ["ANN101", "D200", "D205", "D212"] [tool.ruff.lint.pydocstyle] convention = "google" From e0fa378346b792e47f552a5cbf77a2c7c2fc89c9 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 18 Jul 2024 20:50:15 -0500 Subject: [PATCH 15/76] Lint logger.py - Added type annotations - Missing docstrings (D100, D101, D102, D103, D107) ignored for now --- tools/RAiDER/logger.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tools/RAiDER/logger.py b/tools/RAiDER/logger.py index 483b58b0a..595b5930e 100644 --- a/tools/RAiDER/logger.py +++ b/tools/RAiDER/logger.py @@ -9,9 +9,9 @@ import logging import os -from pathlib import Path import sys -from logging import FileHandler, Formatter, StreamHandler +from logging import FileHandler, Formatter, LogRecord, StreamHandler +from pathlib import Path import RAiDER.cli.conf as conf @@ -26,14 +26,14 @@ class UnixColorFormatter(Formatter): COLORS = {logging.WARNING: yellow, logging.ERROR: red, logging.CRITICAL: bold_red} - def __init__(self, fmt=None, datefmt=None, style='%', use_color=True) -> None: + def __init__(self, fmt: str = None, datefmt: str = None, style: str = '%', use_color: bool=True) -> None: super().__init__(fmt, datefmt, style) # Save the old function so we can call it later self.__formatMessage = self.formatMessage if use_color: self.formatMessage = self.formatMessageColor - def formatMessageColor(self, record): + def formatMessageColor(self, record: LogRecord) -> str: message = self.__formatMessage(record) color = self.COLORS.get(record.levelno) if color: @@ -43,8 +43,7 @@ def formatMessageColor(self, record): class CustomFormatter(UnixColorFormatter): """Adds levelname prefixes to the message on warning or above.""" - - def formatMessage(self, record): + def formatMessage(self, record: LogRecord) -> str: message = super().formatMessage(record) if record.levelno >= logging.WARNING: message = ': '.join((record.levelname, message)) @@ -53,10 +52,10 @@ def formatMessage(self, record): ##################################### # DEFINE THE LOGGER -if conf.LOGGER_PATH is None: - logger_path = Path.cwd() -else: +if conf.LOGGER_PATH is not None: logger_path = conf.LOGGER_PATH +else: + logger_path = Path.cwd() logger = logging.getLogger('RAiDER') logger.setLevel(logging.DEBUG) From 89d8def576da2cb5273227d1b6172921209eb1e3 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 25 Jul 2024 00:17:17 -0500 Subject: [PATCH 16/76] Remove duplicate function test.write_yaml --- test/__init__.py | 2 -- test/test_datelist.py | 3 ++- test/test_intersect.py | 3 ++- test/test_slant.py | 3 ++- test/test_synthetic.py | 3 ++- test/test_temporal_interpolate.py | 3 ++- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index 7d84435f2..71ef256d8 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -7,8 +7,6 @@ import numpy as np import xarray as xr -from RAiDER.utilFcns import write_yaml - test_dir = Path(__file__).parents[0] TEST_DIR = test_dir.absolute() diff --git a/test/test_datelist.py b/test/test_datelist.py index 1201863d7..76739024e 100644 --- a/test/test_datelist.py +++ b/test/test_datelist.py @@ -1,7 +1,8 @@ import datetime import os import shutil -from test import TEST_DIR, WM, write_yaml +from RAiDER.utilFcns import write_yaml +from test import TEST_DIR, WM from RAiDER.cli.raider import read_run_config_file def test_datelist(): diff --git a/test/test_intersect.py b/test/test_intersect.py index 416b24a09..0a2ffed64 100644 --- a/test/test_intersect.py +++ b/test/test_intersect.py @@ -1,3 +1,4 @@ +from RAiDER.utilFcns import write_yaml import pytest import os import pandas as pd @@ -7,7 +8,7 @@ from scipy.interpolate import griddata import rasterio -from test import TEST_DIR, WM_DIR, write_yaml +from test import TEST_DIR, WM_DIR @pytest.mark.parametrize('wm', 'ERA5'.split()) diff --git a/test/test_slant.py b/test/test_slant.py index b5e51104b..b18331fe5 100644 --- a/test/test_slant.py +++ b/test/test_slant.py @@ -8,8 +8,9 @@ import xarray as xr from test import ( - TEST_DIR, WM_DIR, ORB_DIR, write_yaml, make_delay_name + TEST_DIR, WM_DIR, ORB_DIR, make_delay_name ) +from RAiDER.utilFcns import write_yaml @pytest.mark.parametrize('weather_model_name', ['ERA5']) def test_slant_proj(weather_model_name): diff --git a/test/test_synthetic.py b/test/test_synthetic.py index 885161611..1344c3b37 100644 --- a/test/test_synthetic.py +++ b/test/test_synthetic.py @@ -14,9 +14,10 @@ from RAiDER.losreader import Raytracing, build_ray from RAiDER.utilFcns import lla2ecef from RAiDER.cli.validators import modelName2Module +from RAiDER.utilFcns import lla2ecef, write_yaml from test import ( - TEST_DIR, ORB_DIR, WM_DIR, write_yaml + TEST_DIR, ORB_DIR, WM_DIR ) diff --git a/test/test_temporal_interpolate.py b/test/test_temporal_interpolate.py index 488195939..d738ee383 100644 --- a/test/test_temporal_interpolate.py +++ b/test/test_temporal_interpolate.py @@ -8,10 +8,11 @@ from test import ( - WM, TEST_DIR, write_yaml + WM, TEST_DIR ) from RAiDER.logger import logger +from RAiDER.utilFcns import write_yaml wm = 'ERA5' if WM == 'ERA-5' else WM From b5374ebc4d185d98c89fd6c678d39caeaad5d017 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 25 Jul 2024 00:19:50 -0500 Subject: [PATCH 17/76] Remove ineffectual assignment --- test/test_checkArgs.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/test_checkArgs.py b/test/test_checkArgs.py index 07240fc4a..faab8bd53 100644 --- a/test/test_checkArgs.py +++ b/test/test_checkArgs.py @@ -42,7 +42,6 @@ def isWriteable(dirpath): def test_checkArgs_outfmt_1(args): '''Test that passing height levels with hdf5 outformat works''' - args = args args.file_format = 'h5' args.heightlvls = [10, 100, 1000] checkArgs(args) @@ -51,7 +50,6 @@ def test_checkArgs_outfmt_1(args): def test_checkArgs_outfmt_2(args): '''Test that passing a raster format with height levels throws an error''' - args = args args.heightlvs = [10, 100, 1000] args.file_format = 'GTiff' args = checkArgs(args) @@ -60,14 +58,12 @@ def test_checkArgs_outfmt_2(args): def test_checkArgs_outfmt_3(args): '''Test that passing a raster format with height levels throws an error''' - args = args with pytest.raises(FileNotFoundError): args.aoi = StationFile(os.path.join('fake_dir', 'stations.csv')) def test_checkArgs_outfmt_4(args): '''Test that passing a raster format with height levels throws an error''' - args = args args.aoi = RasterRDR( lat_file = os.path.join(SCENARIO_1, 'geom', 'lat.dat'), lon_file = os.path.join(SCENARIO_1, 'geom', 'lon.dat'), @@ -78,7 +74,6 @@ def test_checkArgs_outfmt_4(args): def test_checkArgs_outfmt_5(args): '''Test that passing a raster format with height levels throws an error''' - args = args args.aoi = StationFile(os.path.join(SCENARIO_2, 'stations.csv')) argDict = checkArgs(args) assert pd.read_csv(argDict['wetFilenames'][0]).shape == (8, 4) @@ -86,7 +81,6 @@ def test_checkArgs_outfmt_5(args): def test_checkArgs_outloc_1(args): '''Test that the default output and weather model directories are correct''' - args = args argDict = checkArgs(args) out = argDict['output_directory'] wmLoc = argDict['weather_model_directory'] @@ -97,7 +91,6 @@ def test_checkArgs_outloc_1(args): def test_checkArgs_outloc_2(args, tmp_path): '''Tests that the correct output location gets assigned when provided''' with pushd(tmp_path): - args = args args.output_directory = tmp_path argDict = checkArgs(args) out = argDict['output_directory'] @@ -107,7 +100,6 @@ def test_checkArgs_outloc_2(args, tmp_path): def test_checkArgs_outloc_2b(args, tmp_path): ''' Tests that the weather model directory gets passed through by itself''' with pushd(tmp_path): - args = args args.output_directory = tmp_path args.weather_model_directory = 'weather_dir' argDict = checkArgs(args) @@ -117,7 +109,6 @@ def test_checkArgs_outloc_2b(args, tmp_path): def test_checkArgs_outloc_3(args, tmp_path): '''Tests that the weather model directory gets created when needed''' with pushd(tmp_path): - args = args args.output_directory = tmp_path argDict = checkArgs(args) assert os.path.isdir(argDict['weather_model_directory']) @@ -125,7 +116,6 @@ def test_checkArgs_outloc_3(args, tmp_path): def test_checkArgs_outloc_4(args): '''Tests for creating writeable weather model directory''' - args = args argDict = checkArgs(args) assert isWriteable(argDict['weather_model_directory']) @@ -133,7 +123,6 @@ def test_checkArgs_outloc_4(args): def test_filenames_1(args): '''tests that the correct filenames are generated''' - args = args argDict = checkArgs(args) assert 'Delay' not in argDict['wetFilenames'][0] assert 'wet' in argDict['wetFilenames'][0] @@ -145,7 +134,6 @@ def test_filenames_1(args): def test_filenames_2(args): '''tests that the correct filenames are generated''' - args = args args['output_directory'] = SCENARIO_2 args.aoi = StationFile(os.path.join(SCENARIO_2, 'stations.csv')) argDict = checkArgs(args) From 5dc39c6366501ce803bb76efe365a3a8cc7758dc Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 25 Jul 2024 00:14:02 -0500 Subject: [PATCH 18/76] Formatting --- test/_scenario_1.py | 4 ++-- test/_scenario_2.py | 4 ++-- test/test_datelist.py | 4 +++- test/test_synthetic.py | 6 +++--- test/test_validators.py | 6 +++++- tools/RAiDER/aws.py | 16 +++++++++------- tools/RAiDER/cli/validators.py | 8 ++++---- tools/RAiDER/logger.py | 6 +++++- 8 files changed, 33 insertions(+), 21 deletions(-) diff --git a/test/_scenario_1.py b/test/_scenario_1.py index faaa61063..4f296f1e3 100755 --- a/test/_scenario_1.py +++ b/test/_scenario_1.py @@ -10,7 +10,7 @@ from RAiDER.delay import main from RAiDER.utilFcns import rio_open from RAiDER.checkArgs import makeDelayFileNames -from RAiDER.cli.validators import modelName2Module +from RAiDER.cli.validators import get_wm_by_name SCENARIO_DIR = os.path.join(TEST_DIR, "scenario_1") _RTOL = 1e-2 @@ -93,7 +93,7 @@ def core_test_tropo_delay(tmp_path, modelName): if not os.path.exists(wmLoc): os.mkdir(wmLoc) - _, model_obj = modelName2Module(modelName) + _, model_obj = get_wm_by_name(modelName) wet_file, hydro_file = makeDelayFileNames( time, Zenith, "envi", modelName, tmp_path ) diff --git a/test/_scenario_2.py b/test/_scenario_2.py index c02a77146..4f3cc9fef 100644 --- a/test/_scenario_2.py +++ b/test/_scenario_2.py @@ -9,7 +9,7 @@ from RAiDER.delay import main from RAiDER.losreader import Zenith -from RAiDER.cli.validators import modelName2Module +from RAiDER.cli.validators import get_wm_by_name SCENARIO_DIR = os.path.join(TEST_DIR, "scenario_2") _RTOL = 1e-2 @@ -34,7 +34,7 @@ def test_computeDelay(tmp_path): lats = stats['Lat'].values lons = stats['Lon'].values - _, model_obj = modelName2Module('ERA5') + _, model_obj = get_wm_by_name('ERA5') with pushd(tmp_path): diff --git a/test/test_datelist.py b/test/test_datelist.py index 76739024e..d5baab4e8 100644 --- a/test/test_datelist.py +++ b/test/test_datelist.py @@ -1,6 +1,7 @@ import datetime import os import shutil + from RAiDER.utilFcns import write_yaml from test import TEST_DIR, WM from RAiDER.cli.raider import read_run_config_file @@ -13,7 +14,8 @@ def test_datelist(): dates = ['20200124', '20200130'] true_dates = [ - datetime.datetime(2020,1,24), datetime.datetime(2020,1,30) + datetime.date(2020,1,24), + datetime.date(2020,1,30) ] dct_group = { diff --git a/test/test_synthetic.py b/test/test_synthetic.py index 1344c3b37..9b6bf4bb4 100644 --- a/test/test_synthetic.py +++ b/test/test_synthetic.py @@ -13,8 +13,8 @@ from RAiDER.models.weatherModel import make_weather_model_filename from RAiDER.losreader import Raytracing, build_ray from RAiDER.utilFcns import lla2ecef -from RAiDER.cli.validators import modelName2Module from RAiDER.utilFcns import lla2ecef, write_yaml +from RAiDER.cli.validators import get_wm_by_name from test import ( TEST_DIR, ORB_DIR, WM_DIR @@ -35,7 +35,7 @@ def update_model(wm_file:str, wm_eq_type:str, wm_dir:str='weather_files_synth'): # initialize dummy wm to calculate constant delays # any model will do as 1) all constants same 2) all equations same model = op.basename(wm_file).split('_')[0].upper().replace("-", "") - Obj = modelName2Module(model)[1]() + Obj = get_wm_by_name(model)[1]() ds = xr.open_dataset(wm_file) t = ds['t'] p = ds['p'] @@ -118,7 +118,7 @@ def __post_init__(self): self.dts = self.dt.strftime('%Y_%m_%d_T%H_%M_%S') self.ttime = self.dt.strftime('%H:%M:%S') - self.wmObj = modelName2Module(self.wmName.upper().replace("-", ""))[1]() + self.wmObj = get_wm_by_name(self.wmName.upper().replace("-", ""))[1]() self.hgt_lvls = np.arange(-500, 9500, 500) self._cube_spacing_m = 10000. diff --git a/test/test_validators.py b/test/test_validators.py index 071830cec..099d56471 100644 --- a/test/test_validators.py +++ b/test/test_validators.py @@ -53,7 +53,11 @@ def llarray(): @pytest.fixture def args1(): test_file = os.path.join(SCENARIO, 'los.rdr') - args = AttributeDict({'los_file': test_file, 'los_convention': 'isce','ray_trace': False}) + args = RunConfig( + los_file = test_file, + los_convention = 'isce', + ray_trace = False, + ) return args diff --git a/tools/RAiDER/aws.py b/tools/RAiDER/aws.py index c0d035601..7a2cd2aca 100644 --- a/tools/RAiDER/aws.py +++ b/tools/RAiDER/aws.py @@ -10,14 +10,9 @@ S3_CLIENT = boto3.client('s3') -def get_tag_set(): - tag_set = {'TagSet': [{'Key': 'file_type', 'Value': 'product'}]} - return tag_set - - def get_content_type(file_location: Union[Path, str]) -> str: content_type = guess_type(file_location)[0] - if not content_type: + if content_type is None: content_type = 'application/octet-stream' return content_type @@ -30,7 +25,14 @@ def upload_file_to_s3(path_to_file: Union[str, Path], bucket: str, prefix: str = logger.info(f'Uploading s3://{bucket}/{key}') S3_CLIENT.upload_file(str(path_to_file), bucket, key, extra_args) - tag_set = get_tag_set() + tag_set = { + 'TagSet': [ + { + 'Key': 'file_type', + 'Value': 'product' + } + ] + } S3_CLIENT.put_object_tagging(Bucket=bucket, Key=key, Tagging=tag_set) diff --git a/tools/RAiDER/cli/validators.py b/tools/RAiDER/cli/validators.py index 30493a6f6..4f6843f41 100755 --- a/tools/RAiDER/cli/validators.py +++ b/tools/RAiDER/cli/validators.py @@ -288,7 +288,7 @@ def convert_time(inp): raise ValueError(f'Unable to coerce "{inp}" to a time. Try T%H:%M:%S') -def modelName2Module(model_name): +def get_wm_by_name(model_name: str) -> tuple[str, WeatherModel]: """ Turn an arbitrary string into a module name. @@ -303,9 +303,9 @@ def modelName2Module(model_name): wmObject - callable, weather model object. """ module_name = 'RAiDER.models.' + model_name.lower().replace('-', '') - model_module = importlib.import_module(module_name) - wmObject = getattr(model_module, model_name.upper().replace('-', '')) - return module_name, wmObject + module = importlib.import_module(module_name) + Model = getattr(module, model_name.upper().replace('-', '')) + return module_name, Model def getBufferedExtent(lats, lons=None, buf=0.0): diff --git a/tools/RAiDER/logger.py b/tools/RAiDER/logger.py index 595b5930e..3e7699dea 100644 --- a/tools/RAiDER/logger.py +++ b/tools/RAiDER/logger.py @@ -24,7 +24,11 @@ class UnixColorFormatter(Formatter): bold_red = '\x1b[31;1m' reset = '\x1b[0m' - COLORS = {logging.WARNING: yellow, logging.ERROR: red, logging.CRITICAL: bold_red} + COLORS = { + logging.WARNING: yellow, + logging.ERROR: red, + logging.CRITICAL: bold_red + } def __init__(self, fmt: str = None, datefmt: str = None, style: str = '%', use_color: bool=True) -> None: super().__init__(fmt, datefmt, style) From 928e7c56169c0ba85c11bc357cc0b9e81e454d57 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:58:47 -0500 Subject: [PATCH 19/76] Partially lint utilFcns.py --- tools/RAiDER/utilFcns.py | 65 +++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 37 deletions(-) mode change 100755 => 100644 tools/RAiDER/utilFcns.py diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py old mode 100755 new mode 100644 index ae912ab9f..9371ab091 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -6,12 +6,23 @@ from datetime import datetime, timedelta, timezone from typing import Any -import RAiDER import numpy as np +import rasterio import xarray +import yaml from numpy import ndarray from pyproj import CRS, Proj, Transformer -import yaml + +import RAiDER +from RAiDER.constants import ( + R_EARTH_MAX_WGS84 as Rmax, + R_EARTH_MIN_WGS84 as Rmin, + _THRESHOLD_SECONDS, + _g0 as g0, + _g1 as G1, +) +from RAiDER.logger import logger +from RAiDER.types import BB, RIOProfile # Optional imports @@ -23,26 +34,12 @@ import multiprocessing as mp except ImportError: mp = None -try: - import rasterio -except ImportError: - rasterio = None try: import progressbar except ImportError: progressbar = None -from RAiDER.constants import ( - _g0 as g0, - _g1 as G1, - R_EARTH_MAX_WGS84 as Rmax, - R_EARTH_MIN_WGS84 as Rmin, - _THRESHOLD_SECONDS, -) -from RAiDER.logger import logger - - pbar = None @@ -144,7 +141,7 @@ def rio_profile(fname): return profile -def rio_extents(profile): +def rio_extents(profile: RIOProfile) -> BB.SNWE: """Get a bounding box in SNWE from a rasterio profile.""" gt = profile['transform'].to_gdal() xSize = profile['width'] @@ -154,15 +151,12 @@ def rio_extents(profile): return S, N, W, E -def rio_open(fname, returnProj=False, userNDV=None, band=None): +def rio_open(path: Path, returnProj=False, userNDV=None, band=None): """Reads a rasterio-compatible raster file and returns the data and profile.""" - if rasterio is None: - raise ImportError('RAiDER.utilFcns: rio_open - rasterio is not installed') + if (path / '.vrt').exists(): + path /= '.vrt' - if os.path.exists(fname + '.vrt'): - fname = fname + '.vrt' - - with rasterio.open(fname) as src: + with rasterio.open(path) as src: profile = src.profile # For all bands @@ -204,12 +198,12 @@ def nodataToNan(inarr, listofvals) -> None: inarr[inarr == val] = np.nan -def rio_stats(fname, band=1): +def rio_stats(path: Path, band=1): """ Read a rasterio-compatible file and pull the metadata. Args: - fname - filename to be loaded + fname - file path to be loaded band - band number to use for getting statistics Returns: @@ -217,18 +211,15 @@ def rio_stats(fname, band=1): proj - CRS/projection information for the file gt - geotransform for the data """ - if rasterio is None: - raise ImportError('RAiDER.utilFcns: rio_stats - rasterio is not installed') - - if os.path.basename(fname).startswith('S1-GUNW'): - fname = os.path.join(f'NETCDF:"{fname}":science/grids/data/unwrappedPhase') + if path.name.startswith('S1-GUNW'): + path = Path(f'NETCDF:"{path}":science/grids/data/unwrappedPhase') - if os.path.exists(fname + '.vrt'): - fname = fname + '.vrt' + if (path / '.vrt').exists(): + path = path / '.vrt' # Turn off PAM to avoid creating .aux.xml files with rasterio.Env(GDAL_PAM_ENABLED='NO'): - with rasterio.open(fname) as src: + with rasterio.open(path) as src: gt = src.transform.to_gdal() proj = src.crs stats = src.statistics(band) @@ -236,15 +227,15 @@ def rio_stats(fname, band=1): return stats, proj, gt -def get_file_and_band(filestr): +def get_file_and_band(filestr: str) -> tuple[Path, 1]: """Support file;bandnum as input for filename strings.""" parts = filestr.split(';') # Defaults to first band if no bandnum is provided if len(parts) == 1: - return filestr.strip(), 1 + return Path(filestr.strip()), 1 elif len(parts) == 2: - return parts[0].strip(), int(parts[1].strip()) + return Path(parts[0].strip()), int(parts[1].strip()) else: raise ValueError(f'Cannot interpret {filestr} as valid filename') From 2eba2913483bf69cba183347731a5a097bd14de9 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:06:33 -0500 Subject: [PATCH 20/76] Call raiderDelay as function for debugging --- test/test_slant.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/test_slant.py b/test/test_slant.py index b18331fe5..60eceb4a4 100644 --- a/test/test_slant.py +++ b/test/test_slant.py @@ -1,3 +1,4 @@ +from RAiDER.cli.raider import calcDelays import pytest import glob import os @@ -94,9 +95,7 @@ def test_ray_tracing(weather_model_name): cfg = write_yaml(grp, 'temp.yaml') ## run raider and intersect - cmd = f'raider.py {cfg}' - proc = subprocess.run(cmd.split(), stdout=subprocess.PIPE, universal_newlines=True) - assert proc.returncode == 0, 'RAiDER Failed.' + calcDelays([str(cfg)]) # model to lat/lon/correct value gold = {'ERA5': [33.4, -117.8, 0, 2.97711681]} From 6760f0c47e77156c872d9f72e45ebb00feff8f21 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 25 Jul 2024 00:26:48 -0500 Subject: [PATCH 21/76] Use pathlib.Path Use pathlib.Path Use pathlib.Path Use pathlib.Path --- test/test_GUNW.py | 3 +- test/test_HRRR_ztd.py | 10 +++--- test/test_checkArgs.py | 15 ++++---- test/test_llreader.py | 21 ++++++------ test/test_util.py | 23 ++++++------- tools/RAiDER/aria/calcGUNW.py | 10 +++--- tools/RAiDER/aria/prepFromGUNW.py | 6 ++-- tools/RAiDER/aws.py | 4 +-- tools/RAiDER/checkArgs.py | 14 ++++---- tools/RAiDER/cli/__main__.py | 3 +- tools/RAiDER/cli/conf.py | 5 +-- tools/RAiDER/cli/raider.py | 57 ++++++++++++++++--------------- tools/RAiDER/cli/validators.py | 2 +- tools/RAiDER/llreader.py | 22 +++++++----- tools/RAiDER/utilFcns.py | 39 +++++++++++---------- 15 files changed, 122 insertions(+), 112 deletions(-) diff --git a/test/test_GUNW.py b/test/test_GUNW.py index ebe887a65..af65edb42 100644 --- a/test/test_GUNW.py +++ b/test/test_GUNW.py @@ -18,6 +18,7 @@ import RAiDER.cli.raider as raider import RAiDER.s1_azimuth_timing from RAiDER import aws +import RAiDER.aria.prepFromGUNW from RAiDER.aria.prepFromGUNW import ( check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation, check_weather_model_availability,_get_acq_time_from_gunw_id, @@ -509,7 +510,7 @@ def test_hyp3_exits_succesfully_when_hrrr_not_available(mocker): side_effect=[False]) # The gunw id should not have a hyp3 file associated with it # This call will still hit the HRRR s3 API as done in the previous test - mocker.patch("RAiDER.aws.get_s3_file", side_effect=['hyp3-job-uuid-3ad24/S1-GUNW-A-R-106-tops-20160809_20140101-160001-00078W_00041N-PP-4be8-v3_0_0.nc']) + mocker.patch("RAiDER.aws.get_s3_file", side_effect=[Path('hyp3-job-uuid-3ad24/S1-GUNW-A-R-106-tops-20160809_20140101-160001-00078W_00041N-PP-4be8-v3_0_0.nc')]) mocker.patch('RAiDER.aria.prepFromGUNW.check_weather_model_availability') iargs = [ '--bucket', 's3://foo', diff --git a/test/test_HRRR_ztd.py b/test/test_HRRR_ztd.py index 931cbae77..42ae535b1 100644 --- a/test/test_HRRR_ztd.py +++ b/test/test_HRRR_ztd.py @@ -1,5 +1,3 @@ -import os - from test import TEST_DIR import numpy as np @@ -7,13 +5,13 @@ from RAiDER.cli.raider import calcDelays def test_scenario_1(data_for_hrrr_ztd, mocker): - SCENARIO_DIR = os.path.join(TEST_DIR, "scenario_1") - test_path = os.path.join(SCENARIO_DIR, 'raider_example_1.yaml') + SCENARIO_DIR = TEST_DIR / "scenario_1" + test_path = SCENARIO_DIR / 'raider_example_1.yaml' mocker.patch('RAiDER.processWM.prepareWeatherModel', side_effect=[str(data_for_hrrr_ztd)]) - calcDelays([test_path]) + calcDelays([str(test_path)]) - new_data = xr.load_dataset(os.path.join(SCENARIO_DIR, 'HRRR_tropo_20200101T120000_ztd.nc')) + new_data = xr.load_dataset(SCENARIO_DIR / 'HRRR_tropo_20200101T120000_ztd.nc') new_data1 = new_data.sel(x=-91.84, y=36.84, z=0, method='nearest') golden_data = 2.2622863, 0.0361021 # hydro|wet diff --git a/test/test_checkArgs.py b/test/test_checkArgs.py index faab8bd53..e5202df29 100644 --- a/test/test_checkArgs.py +++ b/test/test_checkArgs.py @@ -30,11 +30,11 @@ def args(): shutil.rmtree(f) if os.path.exists(f) else '' return d -def isWriteable(dirpath): +def isWriteable(dirpath: Path) -> bool: '''Test whether a directory is writeable''' try: - filehandle = open(os.path.join(dirpath, 'tmp.txt'), 'w') - filehandle.close() + with (dirpath / 'tmp.txt').open('w'): + pass return True except IOError: return False @@ -142,17 +142,17 @@ def test_filenames_2(args): def test_makeDelayFileNames_1(): - assert makeDelayFileNames(None, None, "h5", "name", "dir") == \ + assert makeDelayFileNames(None, None, "h5", "name", Path("dir")) == \ ("dir/name_wet_ztd.h5", "dir/name_hydro_ztd.h5") def test_makeDelayFileNames_2(): - assert makeDelayFileNames(None, (), "h5", "name", "dir") == \ + assert makeDelayFileNames(None, (), "h5", "name", Path("dir")) == \ ("dir/name_wet_std.h5", "dir/name_hydro_std.h5") def test_makeDelayFileNames_3(): - assert makeDelayFileNames(datetime.datetime(2020, 1, 1, 1, 2, 3), None, "h5", "model_name", "dir") == \ + assert makeDelayFileNames(datetime.datetime(2020, 1, 1, 1, 2, 3), None, "h5", "model_name", Path("dir")) == \ ( "dir/model_name_wet_20200101T010203_ztd.h5", "dir/model_name_hydro_20200101T010203_ztd.h5" @@ -160,7 +160,7 @@ def test_makeDelayFileNames_3(): def test_makeDelayFileNames_4(): - assert makeDelayFileNames(datetime.datetime(1900, 12, 31, 1, 2, 3), "los", "h5", "model_name", "dir") == \ + assert makeDelayFileNames(datetime.datetime(1900, 12, 31, 1, 2, 3), "los", "h5", "model_name", Path("dir")) == \ ( "dir/model_name_wet_19001231T010203_std.h5", "dir/model_name_hydro_19001231T010203_std.h5" @@ -170,4 +170,3 @@ def test_makeDelayFileNames_4(): def test_get_raster_ext(): with pytest.raises(ValueError): get_raster_ext('dummy_format') - diff --git a/test/test_llreader.py b/test/test_llreader.py index b2699183a..292cc73a8 100644 --- a/test/test_llreader.py +++ b/test/test_llreader.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import pytest import numpy as np @@ -14,9 +15,9 @@ StationFile, RasterRDR, BoundingBox, GeocodedFile, bounds_from_latlon_rasters, bounds_from_csv ) -SCENARIO0_DIR = os.path.join(TEST_DIR, "scenario_0") -SCENARIO1_DIR = os.path.join(TEST_DIR, "scenario_1", "geom") -SCENARIO2_DIR = os.path.join(TEST_DIR, "scenario_2") +SCENARIO0_DIR = TEST_DIR / "scenario_0" +SCENARIO1_DIR = TEST_DIR / "scenario_1/geom" +SCENARIO2_DIR = TEST_DIR / "scenario_2" @pytest.fixture @@ -26,12 +27,12 @@ def parser(): @pytest.fixture def station_file(): - return os.path.join(SCENARIO2_DIR, 'stations.csv') + return SCENARIO2_DIR / 'stations.csv' @pytest.fixture def llfiles(): - return os.path.join(SCENARIO1_DIR, 'lat.dat'), os.path.join(SCENARIO1_DIR, 'lon.dat') + return SCENARIO1_DIR / 'lat.dat', SCENARIO1_DIR / 'lon.dat' def test_latlon_reader_2(): @@ -68,12 +69,12 @@ def test_set_xygrid(): def test_latlon_reader(): - latfile = os.path.join(GEOM_DIR, 'lat.rdr') - lonfile = os.path.join(GEOM_DIR, 'lon.rdr') + latfile = Path(GEOM_DIR) / 'lat.rdr' + lonfile = Path(GEOM_DIR) / 'lon.rdr' lat_true = rio_open(latfile) lon_true = rio_open(lonfile) - query = RasterRDR(lat_file=latfile, lon_file=lonfile) + query = RasterRDR(lat_file=str(latfile), lon_file=str(lonfile)) lats, lons = query.readLL() assert lats.shape == (45, 226) assert lons.shape == (45, 226) @@ -141,10 +142,8 @@ def test_readZ_sf(station_file): def test_GeocodedFile(): - aoi = GeocodedFile(os.path.join(SCENARIO0_DIR, 'small_dem.tif'), is_dem=True) + aoi = GeocodedFile(SCENARIO0_DIR / 'small_dem.tif', is_dem=True) z = aoi.readZ() x,y = aoi.readLL() assert z.shape == (569,558) assert x.shape == z.shape - assert True - diff --git a/test/test_util.py b/test/test_util.py index ca3105878..aada388fb 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -1,5 +1,6 @@ import datetime import os +from pathlib import Path import pytest import numpy as np @@ -21,6 +22,7 @@ _R_EARTH = 6378138 SCENARIO_DIR = os.path.join(TEST_DIR, "scenario_1") +SCENARIO0_DIR = TEST_DIR / "scenario_0" @pytest.fixture @@ -218,16 +220,17 @@ def test_least_nonzero_2(): def test_rio_extent(): # Create a simple georeferenced test file - with rasterio.open("test.tif", mode="w", + test_file = Path("test.tif") + with rasterio.open(test_file, mode="w", width=11, height=11, count=1, dtype=np.float64, crs=pyproj.CRS.from_epsg(4326), transform=rasterio.Affine.from_gdal( 17.0, 0.1, 0, 18.0, 0, -0.1 )) as dst: dst.write(np.random.randn(11, 11), 1) - profile = rio_profile("test.tif") + profile = rio_profile(test_file) assert rio_extents(profile) == (17.0, 18.0, 17.0, 18.0) - os.remove("test.tif") + test_file.unlink() def test_getTimeFromFile(): @@ -485,15 +488,13 @@ def test_get_nearest_wmtimes_4(): def test_rio(): - SCENARIO0_DIR = os.path.join(TEST_DIR, "scenario_0") - geotif = os.path.join(SCENARIO0_DIR, 'small_dem.tif') + geotif = SCENARIO0_DIR / 'small_dem.tif' profile = rio_profile(geotif) assert profile['crs'] is not None def test_rio_2(): - SCENARIO0_DIR = os.path.join(TEST_DIR, "scenario_0") - geotif = os.path.join(SCENARIO0_DIR, 'small_dem.tif') + geotif = SCENARIO0_DIR / 'small_dem.tif' prof = rio_profile(geotif) del prof['transform'] with pytest.raises(KeyError): @@ -501,8 +502,7 @@ def test_rio_2(): def test_rio_3(): - SCENARIO0_DIR = os.path.join(TEST_DIR, "scenario_0") - geotif = os.path.join(SCENARIO0_DIR, 'small_dem.tif') + geotif = SCENARIO0_DIR / 'small_dem.tif' data = rio_open(geotif, returnProj=False, userNDV=None, band=1) assert data.shape == (569,558) @@ -532,8 +532,7 @@ def test_writeArrayToRaster_3(tmp_path): def test_writeArrayToRaster_3(tmp_path): - SCENARIO0_DIR = os.path.join(TEST_DIR, "scenario_0") - geotif = os.path.join(SCENARIO0_DIR, 'small_dem.tif') + geotif = SCENARIO0_DIR / 'small_dem.tif' profile = rio_profile(geotif) data = rio_open(geotif) with pushd(tmp_path): @@ -545,7 +544,7 @@ def test_writeArrayToRaster_3(tmp_path): gt=profile['transform'], fmt='nc', ) - new_fname = os.path.join(tmp_path, 'tmp_file.tif') + new_fname = tmp_path / 'tmp_file.tif' prof = rio_profile(new_fname) assert prof['driver'] == 'GTiff' diff --git a/tools/RAiDER/aria/calcGUNW.py b/tools/RAiDER/aria/calcGUNW.py index dfb045327..abf0748fc 100644 --- a/tools/RAiDER/aria/calcGUNW.py +++ b/tools/RAiDER/aria/calcGUNW.py @@ -111,7 +111,7 @@ def compute_delays_slc(cube_paths: list[Path], wavelength: float) -> xr.Dataset: -def update_gunw_slc(path_gunw: str, ds_slc: xr.Dataset) -> None: +def update_gunw_slc(path_gunw: Path, ds_slc: xr.Dataset) -> None: """Update the path_gunw file using the slc delays in ds_slc.""" with h5py.File(path_gunw, 'a') as h5: for k in TROPO_GROUP.split(): @@ -175,21 +175,21 @@ def update_gunw_slc(path_gunw: str, ds_slc: xr.Dataset) -> None: logger.info('Updated %s group in: %s', os.path.basename(TROPO_GROUP), path_gunw) -def update_gunw_version(path_gunw: str) -> None: +def update_gunw_version(path_gunw: Path) -> None: """Temporary hack for updating version to test aria-tools.""" with netCDF4.Dataset(path_gunw, mode='a') as ds: ds.version = '1c' -def tropo_gunw_slc(cube_paths: list[Path], path_gunw: str, wavelength: float) -> xr.Dataset: +def tropo_gunw_slc(cube_paths: list[Path], path_gunw: Path, wavelength: float) -> xr.Dataset: """ Compute and format the troposphere phase delay for GUNW from RAiDER outputs. Parameters ---------- - cube_filenames : list + cube_filenames : list[Path] list with filename of delay cube for ref and sec date (netcdf) - path_gunw : str + path_gunw : Path GUNW netcdf path wavelength : float Wavelength of SAR diff --git a/tools/RAiDER/aria/prepFromGUNW.py b/tools/RAiDER/aria/prepFromGUNW.py index c67fbded7..34bbe0e6f 100644 --- a/tools/RAiDER/aria/prepFromGUNW.py +++ b/tools/RAiDER/aria/prepFromGUNW.py @@ -78,7 +78,7 @@ def check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id: st return all(ref_dataset_availability) and all(sec_dataset_availability) -def get_slc_ids_from_gunw(gunw_path: str, reference_or_secondary: str = 'reference') -> list[str]: +def get_slc_ids_from_gunw(gunw_path: Path, reference_or_secondary: str = 'reference') -> list[str]: # Example input: test/gunw_test_data/S1-GUNW-D-R-059-tops-20230320_20220418-180300-00179W_00051N-PP-c92e-v2_0_6.nc if reference_or_secondary not in ['reference', 'secondary']: raise ValueError('"reference_or_secondary" must be either "reference" or "secondary"') @@ -94,14 +94,14 @@ def get_acq_time_from_slc_id(slc_id: str) -> pd.Timestamp: return pd.Timestamp(ts_str) -def check_weather_model_availability(gunw_path: str, weather_model_name: str) -> bool: +def check_weather_model_availability(gunw_path: Path, weather_model_name: str) -> bool: """ Check weather reference and secondary dates of GUNW occur within weather model valid range. Parameters ---------- - gunw_path : str + gunw_path : Path weather_model_name : str Should be one of 'HRRR', 'HRES', 'ERA5', 'ERA5T', 'GMAO', 'MERRA2'. diff --git a/tools/RAiDER/aws.py b/tools/RAiDER/aws.py index 7a2cd2aca..c8a308fef 100644 --- a/tools/RAiDER/aws.py +++ b/tools/RAiDER/aws.py @@ -37,7 +37,7 @@ def upload_file_to_s3(path_to_file: Union[str, Path], bucket: str, prefix: str = S3_CLIENT.put_object_tagging(Bucket=bucket, Key=key, Tagging=tag_set) -def get_s3_file(bucket_name: str, bucket_prefix: str, file_type: str) -> Optional[str]: +def get_s3_file(bucket_name: str, bucket_prefix: str, file_type: str) -> Optional[Path]: result = S3_CLIENT.list_objects_v2(Bucket=bucket_name, Prefix=bucket_prefix) for s3_object in result['Contents']: key = s3_object['Key'] @@ -45,4 +45,4 @@ def get_s3_file(bucket_name: str, bucket_prefix: str, file_type: str) -> Optiona file_name = Path(key).name logger.info(f'Downloading s3://{bucket_name}/{key} to {file_name}') S3_CLIENT.download_file(bucket_name, key, file_name) - return file_name + return Path(file_name) diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index c7784730e..11823dc20 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -5,8 +5,8 @@ # RESERVED. United States Government Sponsorship acknowledged. # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -import os from datetime import datetime +from pathlib import Path import pandas as pd import rasterio.drivers as rd @@ -46,9 +46,9 @@ def checkArgs(args): if args.aoi.type() != 'bounding_box': # Handle the GNSS station file if args.aoi.type() == 'station_file': - wetFilename = os.path.join( - args.output_directory, - f'{args.weather_model._dataset.upper()}_Delay' f'_{d.strftime("%Y%m%dT%H%M%S")}_ztd.csv', + wetFilename = str( + run_config.runtime_group.output_directory / + f'{run_config.weather_model._dataset.upper()}_Delay_{d.strftime("%Y%m%dT%H%M%S")}_ztd.csv' ) hydroFilename = '' # only the 'wetFilename' is used for the station_file @@ -107,7 +107,7 @@ def get_raster_ext(fmt): raise ValueError(f'{fmt} is not a valid gdal/rasterio file format for rasters') -def makeDelayFileNames(time, los, outformat, weather_model_name, out): +def makeDelayFileNames(time: Optional[datetime], los: Optional[LOS], outformat: str, weather_model_name: str, out: Path) -> tuple[str, str]: """ return names for the wet and hydrostatic delays. @@ -125,6 +125,6 @@ def makeDelayFileNames(time, los, outformat, weather_model_name, out): ) hydroname, wetname = (format_string.format(dtyp) for dtyp in ('hydro', 'wet')) - hydro_file_name = os.path.join(out, hydroname) - wet_file_name = os.path.join(out, wetname) + hydro_file_name = str(out / hydroname) + wet_file_name = str(out / wetname) return wet_file_name, hydro_file_name diff --git a/tools/RAiDER/cli/__main__.py b/tools/RAiDER/cli/__main__.py index 24c01c101..8786cb0fa 100644 --- a/tools/RAiDER/cli/__main__.py +++ b/tools/RAiDER/cli/__main__.py @@ -24,7 +24,8 @@ def main() -> None: args, unknowns = parser.parse_known_args() # Needed for a global logging path - conf.setLoggerPath(Path(args.logger_path)) + logger_path = Path(args.logger_path) if args.logger_path else None + conf.setLoggerPath(logger_path) sys.argv = [args.process, *unknowns] diff --git a/tools/RAiDER/cli/conf.py b/tools/RAiDER/cli/conf.py index 4d8167b05..eeb50a2c5 100644 --- a/tools/RAiDER/cli/conf.py +++ b/tools/RAiDER/cli/conf.py @@ -1,9 +1,10 @@ -from typing import Optional from pathlib import Path +from typing import Optional + LOGGER_PATH: Optional[Path] = None -def setLoggerPath(path: Path) -> None: +def setLoggerPath(path: Optional[Path]) -> None: global LOGGER_PATH LOGGER_PATH = path diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index a4c6e94a6..5999b5080 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -46,15 +46,15 @@ raider.py run_config_file.yaml """ -DEFAULT_RUN_CONFIG_PATH = os.path.abspath('./raider.yaml') +DEFAULT_RUN_CONFIG_PATH = Path('./examples/template/template.yaml') -def read_run_config_file(fname): +def read_run_config_file(path: Path) -> RunConfig: """ Read the run config file into a dictionary structure. Args: - fname (str): full path to the run config file + path (Path): path to the run config file Returns: dict: arguments to pass to RAiDER functions @@ -64,12 +64,12 @@ def read_run_config_file(fname): """ from RAiDER.cli.validators import enforce_time, enforce_wm, get_heights, get_los, get_query_region, parse_dates - with open(fname) as f: + with path.open() as f: try: params = yaml.safe_load(f) except yaml.YAMLError as exc: print(exc) - raise ValueError(f'Something is wrong with the yaml file {fname}') + raise ValueError(f'Something is wrong with the yaml file {path}') # Drop any values not specified params = drop_nans(params) @@ -180,6 +180,7 @@ def calcDelays(iargs=None) -> list[Path]: group.add_argument( 'run_config_file', nargs='?', + type=lambda p: Path(p).absolute(), help='a YAML file with arguments to RAiDER' ) @@ -192,22 +193,22 @@ def calcDelays(iargs=None) -> list[Path]: if args.generate_config is not None: for filename in ex_run_config_dir.glob('*'): - dest_path = Path(os.getcwd()) / filename.name + dest_path = Path.cwd() / filename.name if dest_path.exists(): print(f'File {dest_path} already exists. Overwrite? [y/n]') if input().lower() != 'y': continue - shutil.copy(filename, os.getcwd()) + shutil.copy(filename, str(Path.cwd())) logger.info('Wrote: %s', filename) sys.exit() # args.generate_config now guaranteed to be None # If no run configuration file is provided, look for a ./raider.yaml if args.run_config_file is not None: - if not os.path.isfile(args.run_config_file): - raise FileNotFoundError(args.run_config_file) + if not args.run_config_file.exists(): + raise FileNotFoundError(str(args.run_config_file)) else: - if not os.path.isfile(DEFAULT_RUN_CONFIG_PATH): + if not DEFAULT_RUN_CONFIG_PATH.is_file(): msg = ( 'No run configuration file provided! Specify a run configuration ' "file or have a 'raider.yaml' file in the current directory." @@ -335,31 +336,32 @@ def calcDelays(iargs=None) -> list[Path]: # A dataset was returned by the above # Dataset returned: Cube e.g. GUNW workflow if hydro_delay is None: + out_filename = Path(out_filename.replace('wet', 'tropo')) ds = wet_delay - ext = os.path.splitext(out_filename)[1] - out_filename = out_filename.replace('wet', 'tropo') + ext = out_filename.suffix # data provenance: include metadata for model and times used times_str = [t.strftime('%Y%m%dT%H:%M:%S') for t in sorted(times)] ds = ds.assign_attrs(model_name=model._Name, model_times_used=times_str, interpolation_method=interp_method) if ext not in ['.nc', '.h5']: - out_filename = f'{os.path.splitext(out_filename)[0]}.nc' + out_filename = out_filename.stem + '.nc' - if out_filename.endswith('.nc'): + if out_filename.suffix == '.nc': ds.to_netcdf(out_filename, mode='w') - elif out_filename.endswith('.h5'): + elif out_filename.suffix == '.h5': ds.to_netcdf(out_filename, engine='h5netcdf', invalid_netcdf=True) logger.info('\nSuccessfully wrote delay cube to: %s\n', out_filename) # Dataset returned: station files, radar_raster, geocoded_file else: + out_filename = Path(out_filename) if aoi.type() == 'station_file': - out_filename = f'{os.path.splitext(out_filename)[0]}.csv' + out_filename = out_filename.stem + '.csv' - if aoi.type() in ['station_file', 'radar_rasters', 'geocoded_file']: - writeDelays(aoi, wet_delay, hydro_delay, out_filename, f, outformat=params['raster_format']) + if aoi.type() in ('station_file', 'radar_rasters', 'geocoded_file'): + writeDelays(aoi, wet_delay, hydro_delay, str(out_filename), f, outformat=run_config.runtime_group.raster_format) - wet_paths.append(Path(out_filename)) + wet_paths.append(out_filename) return wet_paths @@ -542,7 +544,8 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> xr.Dataset: p.add_argument( '-o', '--output-directory', - default=str(Path.cwd()), + default=Path.cwd(), + type=lambda p: Path(p).absolute(), help='Directory to store results.' ) @@ -568,8 +571,7 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> xr.Dataset: args.weather_model == 'HRRR' and args.interpolate_time == 'azimuth_time_grid' ): - file_name = args.file.split('/')[-1] - gunw_id = file_name.replace('.nc', '') + gunw_id = args.file.name.replace('.nc', '') if not RAiDER.aria.prepFromGUNW.check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id): raise NoWeatherModelData('The required HRRR data for time-grid interpolation is not available') @@ -584,8 +586,7 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> xr.Dataset: 'GUNW product file could not be found at' f's3://{args.bucket}/{args.input_bucket_prefix}' ) if args.weather_model == 'HRRR' and args.interpolate_time == 'azimuth_time_grid': - gunw_nc_name = args.file.split('/')[-1] - gunw_id = gunw_nc_name.replace('.nc', '') + gunw_id = args.file.name.replace('.nc', '') if not RAiDER.aria.prepFromGUNW.check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id): print( 'The required HRRR data for time-grid interpolation is not available; returning None and not modifying GUNW dataset' @@ -604,9 +605,11 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> xr.Dataset: raise ValueError( 'GUNW metadata file could not be found at' f's3://{args.bucket}/{args.input_bucket_prefix}' ) - json_data = json.load(open(json_file_path)) + with json_file_path.open() as f: + json_data = json.load(f) json_data['metadata'].setdefault('weather_model', []).append(args.weather_model) - json.dump(json_data, open(json_file_path, 'w')) + with json_file_path.open('w') as f: + json.dump(json_data, f) # also get browse image -- if RAiDER is running in its own HyP3 job, the browse image will be needed for ingest browse_file_path = aws.get_s3_file(args.bucket, args.input_bucket_prefix, '.png') @@ -622,7 +625,7 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> xr.Dataset: # write delay cube (nc) to disk using config # return a list with the path to cube for each date - cube_filenames = calcDelays([path_cfg]) + cube_filenames = calcDelays([str(path_cfg)]) assert len(cube_filenames) == 2, 'Incorrect number of delay files written.' diff --git a/tools/RAiDER/cli/validators.py b/tools/RAiDER/cli/validators.py index 4f6843f41..c67d32ab5 100755 --- a/tools/RAiDER/cli/validators.py +++ b/tools/RAiDER/cli/validators.py @@ -1,11 +1,11 @@ import importlib import itertools -import os import re from argparse import Action, ArgumentError, ArgumentTypeError from datetime import date, datetime, time, timedelta from textwrap import dedent from time import strptime +from pathlib import Path import numpy as np import pandas as pd diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index fdecdef55..66f901d4e 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -6,7 +6,9 @@ # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import os +from pathlib import Path +from RAiDER.types import BB, RIOProfile import numpy as np import pyproj import xarray @@ -262,12 +264,12 @@ def __init__(self, lat_file, lon_file=None, hgt_file=None, dem_file=None, conven def readLL(self): # allow for 2-band lat/lon raster - lats = rio_open(self._latfile) + lats = rio_open(Path(self._latfile)) if self._lonfile is None: return lats else: - return lats, rio_open(self._lonfile) + return lats, rio_open(Path(self._lonfile)) def readZ(self): """Read the heights from the raster file, or download a DEM if not present.""" @@ -308,16 +310,20 @@ def __init__(self, bbox) -> None: class GeocodedFile(AOI): """Parse a Geocoded file for coordinates.""" - def __init__(self, filename, is_dem=False) -> None: + p: RIOProfile + _bounding_box: BB.SNWE + _is_dem: bool + + def __init__(self, path: Path, is_dem=False) -> None: super().__init__() from RAiDER.utilFcns import rio_extents, rio_profile - self._filename = filename - self.p = rio_profile(filename) + self._filename = path + self.p = rio_profile(path) self._bounding_box = rio_extents(self.p) self._is_dem = is_dem - _, self._proj, self._geotransform = rio_stats(filename) + _, self._proj, self._geotransform = rio_stats(path) self._type = 'geocoded_file' try: self.crs = self.p['crs'] @@ -385,8 +391,8 @@ def bounds_from_latlon_rasters(latfile, lonfile): """ from RAiDER.utilFcns import get_file_and_band - latinfo = get_file_and_band(latfile) - loninfo = get_file_and_band(lonfile) + latinfo = get_file_and_band(str(latfile)) + loninfo = get_file_and_band(str(lonfile)) lat_stats, lat_proj, lat_gt = rio_stats(latinfo[0], band=latinfo[1]) lon_stats, lon_proj, lon_gt = rio_stats(loninfo[0], band=loninfo[1]) diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 9371ab091..056045d3d 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -1,10 +1,10 @@ """Geodesy-related utility functions.""" -import os -from pathlib import Path +import pathlib import re from datetime import datetime, timedelta, timezone from typing import Any +from pathlib import Path import numpy as np import rasterio @@ -121,24 +121,18 @@ def ecef2enu(xyz, lat, lon, height): return np.stack((e, n, u), axis=-1) -def rio_profile(fname): +def rio_profile(path: Path) -> RIOProfile: """Reads the profile of a rasterio file.""" - if rasterio is None: - raise ImportError('RAiDER.utilFcns: rio_profile - rasterio is not installed') - - # need to access subdataset directly - if os.path.basename(fname).startswith('S1-GUNW'): - fname = os.path.join(f'NETCDF:"{fname}":science/grids/data/unwrappedPhase') - with rasterio.open(fname) as src: - profile = src.profile + path_vrt = Path(f'{path}.vrt') - elif os.path.exists(fname + '.vrt'): - fname = fname + '.vrt' - - with rasterio.open(fname) as src: - profile = src.profile + if path.name.startswith('S1-GUNW'): + # need to access subdataset directly + path = Path(f'NETCDF:"{path}":science/grids/data/unwrappedPhase') + elif path_vrt.exists(): + path = path_vrt - return profile + with rasterio.open(path) as src: + return src.profile def rio_extents(profile: RIOProfile) -> BB.SNWE: @@ -855,7 +849,15 @@ def get_dt(t1, t2): return np.abs((t1 - t2).total_seconds()) -def write_yaml(content: dict[Any, Any], dst: Path) -> Path: +# Tell PyYAML how to serialize pathlib Paths +yaml.add_representer( + pathlib.PosixPath, + lambda dumper, data: dumper.represent_scalar( + 'tag:yaml.org,2002:str', + str(data) + ) +) +def write_yaml(content: dict[str, Any], dst: Union[str, Path]) -> Path: """Write a new yaml file from a dictionary with template.yaml as a base. Each key-value pair in 'content' will override the one from template.yaml. @@ -871,6 +873,7 @@ def write_yaml(content: dict[Any, Any], dst: Path) -> Path: params = {**params, **content} + dst = Path(dst) with dst.open('w') as fh: yaml.safe_dump(params, fh, default_flow_style=False) From 67fa426039146d3ac78af23a4a45a9a9d1ac91db Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:10:38 -0500 Subject: [PATCH 22/76] Make default group when parameter is None --- tools/RAiDER/cli/raider.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 5999b5080..eff3564aa 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -73,12 +73,11 @@ def read_run_config_file(path: Path) -> RunConfig: # Drop any values not specified params = drop_nans(params) + # Ensure that all the groups exist, even if they are not specified by the user + for key in ('date_group', 'time_group', 'aoi_group', 'height_group', 'los_group', 'runtime_group'): + if key not in yaml_data or yaml_data[key] is None: + yaml_data[key] = {} - # Need to ensure that all the groups exist, even if they are not specified by the user - group_keys = ['date_group', 'time_group', 'aoi_group', 'height_group', 'los_group', 'runtime_group'] - for key in group_keys: - if key not in params.keys(): - params[key] = {} # Parse the user-provided arguments run_config = DEFAULT_DICT.copy() From b1ae30684c4226fb1529084b73ce8a4babcc5033 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:11:48 -0500 Subject: [PATCH 23/76] Remove unnecessary removal of Nones --- tools/RAiDER/cli/raider.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index eff3564aa..06007a324 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -71,8 +71,6 @@ def read_run_config_file(path: Path) -> RunConfig: print(exc) raise ValueError(f'Something is wrong with the yaml file {path}') - # Drop any values not specified - params = drop_nans(params) # Ensure that all the groups exist, even if they are not specified by the user for key in ('date_group', 'time_group', 'aoi_group', 'height_group', 'los_group', 'runtime_group'): if key not in yaml_data or yaml_data[key] is None: From 5ecdf7dbb3c86b9d3ff3ec90d150d8b67a002ebf Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:11:58 -0500 Subject: [PATCH 24/76] Validate look direction --- tools/RAiDER/cli/raider.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 06007a324..a7afc5996 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -76,6 +76,9 @@ def read_run_config_file(path: Path) -> RunConfig: if key not in yaml_data or yaml_data[key] is None: yaml_data[key] = {} + # Validate look direction + if not isinstance(yaml_data['look_dir'], str) or yaml_data['look_dir'].lower() not in ('right', 'left'): + raise ValueError(f'Unknown look direction {yaml_data['look_dir']}') # Parse the user-provided arguments run_config = DEFAULT_DICT.copy() From a98b800d8cdb9afd33121a705f1aa3ec1ebf25ff Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:18:17 -0500 Subject: [PATCH 25/76] Allow yaml to dump tuples --- tools/RAiDER/utilFcns.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 056045d3d..640459c11 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -857,6 +857,14 @@ def get_dt(t1, t2): str(data) ) ) +yaml.add_representer( + tuple, + lambda dumper, data: dumper.represent_sequence( + 'tag:yaml.org,2002:seq', + data + ) +) + def write_yaml(content: dict[str, Any], dst: Union[str, Path]) -> Path: """Write a new yaml file from a dictionary with template.yaml as a base. @@ -875,7 +883,7 @@ def write_yaml(content: dict[str, Any], dst: Union[str, Path]) -> Path: dst = Path(dst) with dst.open('w') as fh: - yaml.safe_dump(params, fh, default_flow_style=False) + yaml.dump(params, fh, default_flow_style=False) logger.info('Wrote new cfg file: %s', str(dst)) return dst From de9a36c1cfb2cb6e5e69c098dd16af0fb8fd67ea Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:00:48 -0500 Subject: [PATCH 26/76] Add types - cli/args.py: types for RAiDER's run configuration and its CLI arguments - types: Look direction, time interpolation method, rasterio profile, bounding box type, and GUNW calc delay arguments --- tools/RAiDER/cli/args.py | 208 +++++++++++++++++++++++++++++++++ tools/RAiDER/cli/raider.py | 4 +- tools/RAiDER/types.py | 36 ------ tools/RAiDER/types/BB.py | 3 + tools/RAiDER/types/__init__.py | 36 ++++++ 5 files changed, 249 insertions(+), 38 deletions(-) create mode 100644 tools/RAiDER/cli/args.py delete mode 100644 tools/RAiDER/types.py create mode 100644 tools/RAiDER/types/BB.py create mode 100644 tools/RAiDER/types/__init__.py diff --git a/tools/RAiDER/cli/args.py b/tools/RAiDER/cli/args.py new file mode 100644 index 000000000..c6d7959a8 --- /dev/null +++ b/tools/RAiDER/cli/args.py @@ -0,0 +1,208 @@ +import argparse +import dataclasses +import datetime as dt +import itertools +import time +from pathlib import Path +from typing import Literal, Optional, Union + +import numpy as np + +from RAiDER.constants import _CUBE_SPACING_IN_M, _ZREF +from RAiDER.llreader import AOI +from RAiDER.losreader import LOS +from RAiDER.models.weatherModel import WeatherModel +from RAiDER.types import BB, LookDir, TimeInterpolationMethod + + +LOSConvention = Literal['isce', 'hyp3'] + +@dataclasses.dataclass +class DateGroupUnparsed: + date_start: Optional[Union[int, str]] = None + date_end: Optional[Union[int, str]] = None + date_step: Optional[Union[int, str]] = None + date_list: Optional[Union[int, str]] = None + +@dataclasses.dataclass +class DateGroup: + # After the dates have been parsed, only the date list is valid, and all + # other fields of the date group should not be used. + date_list: list[dt.date] + + +class TimeGroup: + """Parse an input time (required to be ISO 8601).""" + _DEFAULT_ACQUISITION_WINDOW_SEC = 30 + TIME_FORMATS = ( + '', + 'T%H:%M:%S.%f', + 'T%H%M%S.%f', + '%H%M%S.%f', + 'T%H:%M:%S', + '%H:%M:%S', + 'T%H%M%S', + '%H%M%S', + 'T%H:%M', + 'T%H%M', + '%H:%M', + 'T%H', + ) + TIMEZONE_FORMATS = ( + '', + 'Z', + '%z', + ) + time: dt.time + end_time: dt.time + interpolate_time: Optional[TimeInterpolationMethod] + + def __init__( + self, + time: Optional[Union[str, dt.time]] = None, + end_time: Optional[Union[str, dt.time]] = None, + interpolate_time: Optional[TimeInterpolationMethod] = None, + ) -> None: + self.interpolate_time = interpolate_time + + if time is None: + raise ValueError('You must specify a "time" in the input config file') + if isinstance(time, dt.time): + self.time = time + else: + self.time = TimeGroup.coerce_into_time(time) + + if end_time is not None: + if isinstance(end_time, dt.time): + self.end_time = end_time + else: + self.end_time = TimeGroup.coerce_into_time(end_time) + if self.end_time < self.time: + raise ValueError( + 'Acquisition start time must be before end time. ' + f'Provided start time {self.time} is later than end time {self.end_time}' + ) + else: + sentinel_datetime = dt.datetime.combine(dt.date(1900, 1, 1), self.time) + end_time = sentinel_datetime + dt.timedelta(seconds=TimeGroup._DEFAULT_ACQUISITION_WINDOW_SEC) + self.end_time = end_time.time() + if self.end_time < self.time: + raise ValueError( + 'Acquisition start time must be before end time. ' + f'Provided start time {self.time} is later than end time {self.end_time} ' + f'(with default window of {TimeGroup._DEFAULT_ACQUISITION_WINDOW_SEC} seconds)' + ) + + @staticmethod + def coerce_into_time(val: Union[int, str]) -> dt.time: + all_formats = map(''.join, itertools.product(TimeGroup.TIME_FORMATS, TimeGroup.TIMEZONE_FORMATS)) + for tf in all_formats: + try: + return dt.time(*time.strptime(val, tf)[3:6]) + except ValueError: + pass + raise ValueError(f'Unable to coerce "{val}" to a time. Try T%H:%M:%S') + +@dataclasses.dataclass +class AOIGroupUnparsed: + bounding_box: Optional[Union[str, list[Union[float, int]], BB.SNWE]] = None + geocoded_file: Optional[str] = None + lat_file: Optional[str] = None + lon_file: Optional[str] = None + station_file: Optional[str] = None + geo_cube: Optional[str] = None + +@dataclasses.dataclass +class AOIGroup: + # Once the AOI group is parsed, the members from the config file should not + # be read again. Instead, the parsed AOI will be available on AOIGroup.aoi. + aoi: AOI + + +@dataclasses.dataclass +class HeightGroupUnparsed: + dem: Optional[str] = None + use_dem_latlon: bool = False + height_file_rdr: Optional[str] = None + height_levels: Optional[Union[str, list[Union[float, int]]]] = None + +@dataclasses.dataclass +class HeightGroup: + dem: Optional[str] + use_dem_latlon: bool + height_file_rdr: Optional[str] + height_levels: Optional[list[float]] + + +@dataclasses.dataclass +class LOSGroupUnparsed: + ray_trace: bool = False + los_file: Optional[str] = None + los_convention: LOSConvention = 'isce' + los_cube: Optional[str] = None + orbit_file: Optional[str] = None + zref: np.float64 = _ZREF + +@dataclasses.dataclass +class LOSGroup: + los: LOS + ray_trace: bool = False + los_file: Optional[str] = None + los_convention: LOSConvention = 'isce' + los_cube: Optional[str] = None + orbit_file: Optional[str] = None + zref: np.float64 = _ZREF + +class RuntimeGroup: + raster_format: str + file_format: str # TODO(garlic-os): redundant with raster_format? + verbose: bool + output_projection: str + cube_spacing_in_m: float + download_only: bool + output_directory: Path + weather_model_directory: Path + + def __init__( + self, + raster_format: str = 'GTiff', + file_format: str = 'GTiff', + verbose: bool = True, + output_projection: str = 'EPSG:4326', + cube_spacing_in_m: float = _CUBE_SPACING_IN_M, + download_only: bool = False, + output_directory: str = '.', + weather_model_directory: Optional[str] = None, + ): + self.raster_format = raster_format + self.file_format = file_format + self.verbose = verbose + self.output_projection = output_projection + self.cube_spacing_in_m = cube_spacing_in_m + self.download_only = download_only + self.output_directory = Path(output_directory) + if weather_model_directory is not None: + self.weather_model_directory = Path(weather_model_directory) + else: + self.weather_model_directory = self.output_directory / 'weather_files' + + +@dataclasses.dataclass +class RunConfig: + weather_model: WeatherModel + date_group: DateGroup + time_group: TimeGroup + aoi_group: AOIGroup + height_group: HeightGroup + los_group: LOSGroup + runtime_group: RuntimeGroup + look_dir: LookDir = 'right' + cube_spacing_in_m: Optional[float] = None # deprecated + wetFilenames: Optional[list[str]] = None + hydroFilenames: Optional[list[str]] = None + + +class RAiDERArgs(argparse.Namespace): + download_only: bool = False + generate_config: Optional[str] = None + run_config_file: Optional[Path] diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index a7afc5996..553eab867 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -8,7 +8,7 @@ from textwrap import dedent from typing import Literal, Optional, cast -from RAiDER.types import CalcDelaysArgs, CalcDelaysArgsUnvalidated, TimeInterpolationMethod +from RAiDER.types import CalcDelaysArgs, CalcDelaysArgsUnparsed, TimeInterpolationMethod import numpy as np import xarray as xr import yaml @@ -549,7 +549,7 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> xr.Dataset: help='Directory to store results.' ) - args: CalcDelaysArgsUnvalidated = p.parse_args(iargs, namespace=CalcDelaysArgsUnvalidated()) + args: CalcDelaysArgsUnparsed = p.parse_args(iargs, namespace=CalcDelaysArgsUnparsed()) if args.input_bucket_prefix is None: args.input_bucket_prefix = args.bucket_prefix diff --git a/tools/RAiDER/types.py b/tools/RAiDER/types.py deleted file mode 100644 index d16ed3126..000000000 --- a/tools/RAiDER/types.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Types specific to RAiDER.""" - -import argparse -from dataclasses import dataclass -from typing import Literal, Optional - - -@dataclass -class BB: - SNWE = tuple[float, float, float, float] - -LookDir = Literal['right', 'left'] - -TimeInterpolationMethod = Literal['none', 'center_time', 'azimuth_time_grid'] - -class CalcDelaysArgsUnvalidated(argparse.Namespace): - bucket: Optional[str] - bucket_prefix: Optional[str] - input_bucket_prefix: Optional[str] - file: Optional[str] - weather_model: str - api_uid: Optional[str] - api_key: Optional[str] - interpolate_time: str - output_directory: str - -class CalcDelaysArgs(CalcDelaysArgsUnvalidated): - bucket: Optional[str] - bucket_prefix: Optional[str] - input_bucket_prefix: Optional[str] - file: str - weather_model: str - api_uid: Optional[str] - api_key: Optional[str] - interpolate_time: TimeInterpolationMethod - output_directory: str \ No newline at end of file diff --git a/tools/RAiDER/types/BB.py b/tools/RAiDER/types/BB.py new file mode 100644 index 000000000..92b7f1ec6 --- /dev/null +++ b/tools/RAiDER/types/BB.py @@ -0,0 +1,3 @@ +SNWE = tuple[float, float, float, float] +SN = tuple[float, float] +WE = tuple[float, float] diff --git a/tools/RAiDER/types/__init__.py b/tools/RAiDER/types/__init__.py new file mode 100644 index 000000000..e8be5b6f6 --- /dev/null +++ b/tools/RAiDER/types/__init__.py @@ -0,0 +1,36 @@ +"""Types specific to RAiDER.""" + +import argparse +from pathlib import Path +from typing import Literal, Optional, TypedDict, Union + +import rasterio.crs +import rasterio.transform + + +LookDir = Literal['right', 'left'] + +TimeInterpolationMethod = Literal['none', 'center_time', 'azimuth_time_grid'] + +class CalcDelaysArgsUnparsed(argparse.Namespace): + bucket: Optional[str] + bucket_prefix: Optional[str] + input_bucket_prefix: Optional[str] + file: Optional[Path] + weather_model: str + api_uid: Optional[str] + api_key: Optional[str] + interpolate_time: TimeInterpolationMethod + output_directory: Path + +class CalcDelaysArgs(CalcDelaysArgsUnparsed): + file: Path + +class RIOProfile(TypedDict): + driver: str + width: int + height: int + count: int + crs: Union[str, dict, rasterio.crs.CRS] + transform: rasterio.transform.Affine + dtype: str From b1a8844fe6e93fe659c302887e157b33c752e4cd Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:34:51 -0500 Subject: [PATCH 27/76] Update test_validators.py --- test/test_validators.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_validators.py b/test/test_validators.py index 099d56471..cf37bc7d0 100644 --- a/test/test_validators.py +++ b/test/test_validators.py @@ -53,11 +53,11 @@ def llarray(): @pytest.fixture def args1(): test_file = os.path.join(SCENARIO, 'los.rdr') - args = RunConfig( - los_file = test_file, - los_convention = 'isce', - ray_trace = False, - ) + args = { + 'los_file': test_file, + 'los_convention': 'isce', + 'ray_trace': False, + } return args From b8b6bd8afd87a5404149638f3479413e57e87a0d Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:35:56 -0500 Subject: [PATCH 28/76] Fix merge conflict --- tools/RAiDER/checkArgs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index 11823dc20..dbd479d50 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -19,7 +19,6 @@ def checkArgs(args): """ Check argument compatibility and return the correct variables. """ - ######################################################################################################################### # Directories if args.weather_model_directory is None: args.weather_model_directory = os.path.join(args.output_directory, 'weather_files') From b3d801126fa7c283bac2ccb82702a90e79909964 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:16:51 -0500 Subject: [PATCH 29/76] Add type annotations Add type annotations Add type annotations Add type annotations Add type annotations Add type annotations Add type annotations Add type annotations Add type annotations Add type annotations --- test/test_checkArgs.py | 1 + test/test_synthetic.py | 1 - test/test_validators.py | 2 +- test/test_weather_model.py | 6 ++++-- tools/RAiDER/checkArgs.py | 1 + tools/RAiDER/cli/raider.py | 16 +++++++++------- tools/RAiDER/cli/validators.py | 3 +++ tools/RAiDER/llreader.py | 3 ++- tools/RAiDER/models/hrrr.py | 7 +------ tools/RAiDER/models/weatherModel.py | 26 +++++++++----------------- tools/RAiDER/utilFcns.py | 2 +- 11 files changed, 32 insertions(+), 36 deletions(-) diff --git a/test/test_checkArgs.py b/test/test_checkArgs.py index e5202df29..228100d2c 100644 --- a/test/test_checkArgs.py +++ b/test/test_checkArgs.py @@ -1,5 +1,6 @@ import datetime import os +from pathlib import Path import shutil import pytest diff --git a/test/test_synthetic.py b/test/test_synthetic.py index 9b6bf4bb4..adb164067 100644 --- a/test/test_synthetic.py +++ b/test/test_synthetic.py @@ -12,7 +12,6 @@ from RAiDER.llreader import BoundingBox from RAiDER.models.weatherModel import make_weather_model_filename from RAiDER.losreader import Raytracing, build_ray -from RAiDER.utilFcns import lla2ecef from RAiDER.utilFcns import lla2ecef, write_yaml from RAiDER.cli.validators import get_wm_by_name diff --git a/test/test_validators.py b/test/test_validators.py index cf37bc7d0..4ccb9a8ca 100644 --- a/test/test_validators.py +++ b/test/test_validators.py @@ -1,5 +1,5 @@ from argparse import ArgumentParser -from datetime import datetime, time +from datetime import datetime, time, date import os import pytest diff --git a/test/test_weather_model.py b/test/test_weather_model.py index 6bf0b8b56..a403c0710 100644 --- a/test/test_weather_model.py +++ b/test/test_weather_model.py @@ -315,7 +315,8 @@ def test_hrrr(hrrr: HRRR): wm.checkTime(datetime.datetime(2010, 7, 15).replace(tzinfo=datetime.timezone(offset=datetime.timedelta()))) wm.checkTime(datetime.datetime(2018, 7, 12).replace(tzinfo=datetime.timezone(offset=datetime.timedelta()))) - assert isinstance(wm.checkValidBounds([35, 40, -95, -90]), HRRR) + assert isinstance(wm, HRRR) + wm.checkValidBounds(np.array([35, 40, -95, -90])) with pytest.raises(ValueError): wm.checkValidBounds([45, 47, 300, 310]) @@ -326,7 +327,8 @@ def test_hrrrak(hrrrak: HRRRAK): assert wm._Name == 'HRRR-AK' assert wm._valid_range[0] == datetime.datetime(2018, 7, 13).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())) - assert isinstance(wm.checkValidBounds([45, 47, 200, 210]), HRRRAK) + assert isinstance(wm, HRRRAK) + wm.checkValidBounds(np.array([45, 47, 200, 210])) with pytest.raises(ValueError): wm.checkValidBounds([15, 20, 265, 270]) diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index dbd479d50..ef4ccdd48 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -7,6 +7,7 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ from datetime import datetime from pathlib import Path +from typing import Optional import pandas as pd import rasterio.drivers as rd diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 553eab867..22a586fe9 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -4,11 +4,11 @@ import os import shutil import sys +from collections.abc import Sequence from pathlib import Path from textwrap import dedent -from typing import Literal, Optional, cast +from typing import Any, Optional, cast -from RAiDER.types import CalcDelaysArgs, CalcDelaysArgsUnparsed, TimeInterpolationMethod import numpy as np import xarray as xr import yaml @@ -20,6 +20,7 @@ from RAiDER.cli.parser import add_cpus, add_out, add_verbose from RAiDER.cli.validators import DateListAction, date_type from RAiDER.logger import logger, logging +from RAiDER.losreader import Raytracing from RAiDER.models.allowed import ALLOWED_MODELS from RAiDER.models.customExceptions import DatetimeFailed, NoWeatherModelData, TryToKeepGoingError, WrongNumberOfFiles from RAiDER.s1_azimuth_timing import ( @@ -27,6 +28,7 @@ get_s1_azimuth_time_grid, get_times_for_azimuth_interpolation, ) +from RAiDER.types import CalcDelaysArgs, CalcDelaysArgsUnparsed from RAiDER.utilFcns import get_dt @@ -49,7 +51,7 @@ DEFAULT_RUN_CONFIG_PATH = Path('./examples/template/template.yaml') -def read_run_config_file(path: Path) -> RunConfig: +def read_run_config_file(path: Path) -> AttributeDict: """ Read the run config file into a dictionary structure. @@ -66,7 +68,7 @@ def read_run_config_file(path: Path) -> RunConfig: with path.open() as f: try: - params = yaml.safe_load(f) + yaml_data: dict[str, Any] = yaml.safe_load(f) except yaml.YAMLError as exc: print(exc) raise ValueError(f'Something is wrong with the yaml file {path}') @@ -127,7 +129,7 @@ def read_run_config_file(path: Path) -> RunConfig: return AttributeDict(run_config) -def drop_nans(d): +def drop_nans(d: dict[str, Any]) -> dict[str, Any]: for key in list(d.keys()): if d[key] is None: del d[key] @@ -138,7 +140,7 @@ def drop_nans(d): return d -def calcDelays(iargs=None) -> list[Path]: +def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: """Parse command line arguments using argparse.""" import RAiDER import RAiDER.processWM @@ -475,7 +477,7 @@ def downloadGNSS() -> None: # ------------------------------------------------------------ prepFromGUNW.py -def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> xr.Dataset: +def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> Optional[xr.Dataset]: p = argparse.ArgumentParser( description='Calculate a cube of interferometic delays for GUNW files', formatter_class=argparse.ArgumentDefaultsHelpFormatter, diff --git a/tools/RAiDER/cli/validators.py b/tools/RAiDER/cli/validators.py index c67d32ab5..5c5097527 100755 --- a/tools/RAiDER/cli/validators.py +++ b/tools/RAiDER/cli/validators.py @@ -6,6 +6,7 @@ from textwrap import dedent from time import strptime from pathlib import Path +from typing import Any, Optional, Self, Union import numpy as np import pandas as pd @@ -13,6 +14,8 @@ from RAiDER.llreader import BoundingBox, GeocodedFile, Geocube, RasterRDR, StationFile from RAiDER.logger import logger from RAiDER.losreader import Conventional, Zenith +from RAiDER.models.weatherModel import WeatherModel +from RAiDER.types import BB from RAiDER.utilFcns import rio_extents, rio_profile diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index 66f901d4e..ce06868bc 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -7,6 +7,7 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import os from pathlib import Path +from typing import Union from RAiDER.types import BB, RIOProfile import numpy as np @@ -165,7 +166,7 @@ def calc_buffer_ray(self, direction, lookDir='right', incAngle=30, maxZ=80, digi def set_output_directory(self, output_directory) -> None: self._output_directory = output_directory - def set_output_xygrid(self, dst_crs=4326) -> None: + def set_output_xygrid(self, dst_crs: Union[int, str]=4326) -> None: """Define the locations where the delays will be returned.""" from RAiDER.utilFcns import transform_bbox diff --git a/tools/RAiDER/models/hrrr.py b/tools/RAiDER/models/hrrr.py index 50bc526ed..4fcfa755f 100644 --- a/tools/RAiDER/models/hrrr.py +++ b/tools/RAiDER/models/hrrr.py @@ -288,16 +288,13 @@ def load_weather(self, f=None, *args, **kwargs) -> None: self._lons = _lons self._proj = proj - def checkValidBounds(self: WeatherModel, ll_bounds: np.ndarray): + def checkValidBounds(self, ll_bounds: np.ndarray) -> None: """ Checks whether the given bounding box is valid for the HRRR or HRRRAK (i.e., intersects with the model domain at all). Args: ll_bounds : np.ndarray - - Returns: - The weather model object """ S, N, W, E = ll_bounds aoi = box(W, S, E, N) @@ -321,8 +318,6 @@ def checkValidBounds(self: WeatherModel, ll_bounds: np.ndarray): else: raise ValueError('The requested location is unavailable for HRRR') - return Mod - class HRRRAK(WeatherModel): def __init__(self) -> None: diff --git a/tools/RAiDER/models/weatherModel.py b/tools/RAiDER/models/weatherModel.py index f349262c3..359dc6a27 100755 --- a/tools/RAiDER/models/weatherModel.py +++ b/tools/RAiDER/models/weatherModel.py @@ -1,6 +1,7 @@ import datetime import os from abc import ABC, abstractmethod +from typing import Optional import numpy as np import xarray @@ -15,7 +16,6 @@ from RAiDER.interpolator import fillna3D from RAiDER.logger import logger from RAiDER.models import plotWeather as plots -from RAiDER.models import weatherModel from RAiDER.models.customExceptions import DatetimeOutsideRange from RAiDER.utilFcns import calcgeoh, clip_bbox, robmax, robmin, transform_coords @@ -34,6 +34,8 @@ class WeatherModel(ABC): """Implement a generic weather model for getting estimated SAR delays.""" + _dataset: Optional[str] + def __init__(self) -> None: # Initialize model-specific constants/parameters self._k1 = None @@ -445,36 +447,26 @@ def bbox(self) -> list: return self._bbox def checkValidBounds( - self: weatherModel, + self, ll_bounds: np.ndarray, - ): - """ - Checks whether the given bounding box is valid for the model - (i.e., intersects with the model domain at all). + ) -> None: + """Check whether the given bounding box is valid for the model (i.e., intersects with the model domain at all). Args: ll_bounds : np.ndarray - - Returns: - The weather model object """ S, N, W, E = ll_bounds - if box(W, S, E, N).intersects(self._valid_bounds): - Mod = self - - else: + if not box(W, S, E, N).intersects(self._valid_bounds): raise ValueError(f'The requested location is unavailable for {self._Name}') - return Mod - - def checkContainment(self: weatherModel, ll_bounds, buffer_deg: float = 1e-5) -> bool: + def checkContainment(self, ll_bounds, buffer_deg: float = 1e-5) -> bool: """ " Checks containment of weather model bbox of outLats and outLons provided. Args: ---------- - weather_model : weatherModel + weather_model : WeatherModel ll_bounds: an array of floats (SNWE) demarcating bbox of targets buffer_deg : float For x-translates for extents that lie outside of world bounding box, diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 640459c11..90aac6504 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -3,8 +3,8 @@ import pathlib import re from datetime import datetime, timedelta, timezone -from typing import Any from pathlib import Path +from typing import Any, Union import numpy as np import rasterio From 6212c413c55abf24424d1669da0026af09d68e79 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:41:12 -0500 Subject: [PATCH 30/76] Fix merge conflict --- tools/RAiDER/checkArgs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index ef4ccdd48..687486dfe 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -5,6 +5,7 @@ # RESERVED. United States Government Sponsorship acknowledged. # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +import os from datetime import datetime from pathlib import Path from typing import Optional From b0ad0707441a6d7c31e8f023ada954b19da8f328 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:42:42 -0500 Subject: [PATCH 31/76] Add type annotations --- tools/RAiDER/checkArgs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index 687486dfe..012e91da6 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -13,8 +13,9 @@ import pandas as pd import rasterio.drivers as rd +from RAiDER.llreader import BoundingBox, StationFile from RAiDER.logger import logger -from RAiDER.losreader import Zenith +from RAiDER.losreader import LOS, Zenith def checkArgs(args): From 016df98c89d3fe3e9aa204ec83c50bb99b905ab8 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:49:02 -0500 Subject: [PATCH 32/76] Fix merge conflict --- tools/RAiDER/checkArgs.py | 4 ++-- tools/RAiDER/cli/raider.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index 012e91da6..3405b0e11 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -49,8 +49,8 @@ def checkArgs(args): # Handle the GNSS station file if args.aoi.type() == 'station_file': wetFilename = str( - run_config.runtime_group.output_directory / - f'{run_config.weather_model._dataset.upper()}_Delay_{d.strftime("%Y%m%dT%H%M%S")}_ztd.csv' + args.output_directory / + f'{args.weather_model._dataset.upper()}_Delay_{d.strftime("%Y%m%dT%H%M%S")}_ztd.csv' ) hydroFilename = '' # only the 'wetFilename' is used for the station_file diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 22a586fe9..9468c6af7 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -84,7 +84,7 @@ def read_run_config_file(path: Path) -> AttributeDict: # Parse the user-provided arguments run_config = DEFAULT_DICT.copy() - for key, value in params.items(): + for key, value in yaml_data.items(): if key == 'runtime_group': for k, v in value.items(): if v is not None: @@ -95,7 +95,7 @@ def read_run_config_file(path: Path) -> AttributeDict: run_config['date_list'] = parse_dates(AttributeDict(value)) if key == 'aoi_group': # in case a DEM is passed and should be used - dct_temp = {**AttributeDict(value), **AttributeDict(params['height_group'])} + dct_temp = {**AttributeDict(value), **AttributeDict(yaml_data['height_group'])} run_config['aoi'] = get_query_region(AttributeDict(dct_temp)) if key == 'los_group': @@ -111,7 +111,7 @@ def read_run_config_file(path: Path) -> AttributeDict: run_config[key] = bool(value) # Have to guarantee that certain variables exist prior to looking at heights - for key, value in params.items(): + for key, value in yaml_data.items(): if key == 'height_group': run_config.update( get_heights( From 5fe4e0f50364c9ebdb98d071acd47b7a9c6a1240 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:08:38 -0500 Subject: [PATCH 33/76] Type annotate RAiDER run configuration This change affects the configuration options passed as a YAML file that RAiDER uses to run. Seeing that that is integral to lots of RAiDERs' functionality, it has touched a lot of files. - Run configuration has been changed from an untyped AttributeDict to a typed dataclass called RunConfig. Right now, checking and validation work is split across several functions in validators.py, and in checkArgs and raider.py, and in smaller places across the program. Storing run configuration in a dataclass may open up possibilities for some of the argument parsing and validating code to be simplified and centralized to the class's constructor. - Changed --download_only's default from None to False - Made use of pathlib.Path in a few more places - Changed some instances of the pattern `if x in [list, of, options]:` to use a tuple instead of a list (runs slightly faster at no cost) - coerce_into_date now returns a date and not a datetime, as the time portion was unused. - date_type has been changed to use coerce_into_date under the hood, whereas before it copied its logic and was an instance of data duplication. - Added comments I apologize if some of the input validation/parsing is sloppier than it was before; I made just the changes necessary to allow it to work with the dataclass approach. The project is now open for future work where this part of the program can be simplified. --- test/test_checkArgs.py | 82 +++--- test/test_datelist.py | 10 +- test/test_synthetic.py | 6 +- test/test_util.py | 8 +- test/test_validators.py | 68 ++--- test/test_weather_model.py | 4 +- tools/RAiDER/aria/prepFromGUNW.py | 3 +- tools/RAiDER/checkArgs.py | 83 +++--- tools/RAiDER/cli/__init__.py | 45 --- tools/RAiDER/cli/raider.py | 176 +++++++----- tools/RAiDER/cli/validators.py | 454 +++++++++++++----------------- 11 files changed, 436 insertions(+), 503 deletions(-) diff --git a/test/test_checkArgs.py b/test/test_checkArgs.py index 228100d2c..d68ac62ab 100644 --- a/test/test_checkArgs.py +++ b/test/test_checkArgs.py @@ -8,7 +8,7 @@ from test import TEST_DIR, pushd -from RAiDER.cli import DEFAULT_DICT +from RAiDER.cli.args import AOIGroup, DateGroup, HeightGroupUnparsed, LOSGroup, RunConfig, RuntimeGroup, TimeGroup from RAiDER.checkArgs import checkArgs, makeDelayFileNames, get_raster_ext from RAiDER.llreader import BoundingBox, StationFile, RasterRDR from RAiDER.losreader import Zenith @@ -20,12 +20,15 @@ @pytest.fixture(autouse=True) def args(): - d = DEFAULT_DICT - d['date_list'] = [datetime.datetime(2018, 1, 1)] - d['time'] = datetime.time(12,0,0) - d['aoi'] = BoundingBox([38, 39, -92, -91]) - d['los'] = Zenith() - d['weather_model'] = GMAO() + d = RunConfig( + weather_model=GMAO(), + date_group=DateGroup(date_list=[datetime.datetime(2018, 1, 1)]), + time_group=TimeGroup(time=datetime.time(12,0,0)), + aoi_group=AOIGroup(aoi=BoundingBox([38, 39, -92, -91])), + los_group=LOSGroup(los=Zenith()), + height_group=HeightGroupUnparsed(), + runtime_group=RuntimeGroup() + ) for f in 'weather_files weather_dir'.split(): shutil.rmtree(f) if os.path.exists(f) else '' @@ -43,16 +46,16 @@ def isWriteable(dirpath: Path) -> bool: def test_checkArgs_outfmt_1(args): '''Test that passing height levels with hdf5 outformat works''' - args.file_format = 'h5' - args.heightlvls = [10, 100, 1000] - checkArgs(args) + args.runtime_group.file_format = 'h5' + args.height_group.height_levels = [10, 100, 1000] + args = checkArgs(args) assert os.path.splitext(args.wetFilenames[0])[-1] == '.h5' def test_checkArgs_outfmt_2(args): '''Test that passing a raster format with height levels throws an error''' - args.heightlvs = [10, 100, 1000] - args.file_format = 'GTiff' + args.runtime_group.file_format = 'GTiff' + args.height_group.height_levels = [10, 100, 1000] args = checkArgs(args) assert os.path.splitext(args.wetFilenames[0])[-1] == '.nc' @@ -60,31 +63,31 @@ def test_checkArgs_outfmt_2(args): def test_checkArgs_outfmt_3(args): '''Test that passing a raster format with height levels throws an error''' with pytest.raises(FileNotFoundError): - args.aoi = StationFile(os.path.join('fake_dir', 'stations.csv')) + args.aoi_group.aoi = StationFile(os.path.join('fake_dir', 'stations.csv')) def test_checkArgs_outfmt_4(args): '''Test that passing a raster format with height levels throws an error''' - args.aoi = RasterRDR( + args.aoi_group.aoi = RasterRDR( lat_file = os.path.join(SCENARIO_1, 'geom', 'lat.dat'), lon_file = os.path.join(SCENARIO_1, 'geom', 'lon.dat'), ) - argDict = checkArgs(args) - assert argDict.aoi.type()=='radar_rasters' + args = checkArgs(args) + assert args.aoi_group.aoi.type()=='radar_rasters' def test_checkArgs_outfmt_5(args): '''Test that passing a raster format with height levels throws an error''' - args.aoi = StationFile(os.path.join(SCENARIO_2, 'stations.csv')) - argDict = checkArgs(args) - assert pd.read_csv(argDict['wetFilenames'][0]).shape == (8, 4) + args.aoi_group.aoi = StationFile(os.path.join(SCENARIO_2, 'stations.csv')) + args = checkArgs(args) + assert pd.read_csv(args.wetFilenames[0]).shape == (8, 4) def test_checkArgs_outloc_1(args): '''Test that the default output and weather model directories are correct''' argDict = checkArgs(args) - out = argDict['output_directory'] - wmLoc = argDict['weather_model_directory'] + out = argDict.runtime_group.output_directory + wmLoc = argDict.runtime_group.weather_model_directory assert os.path.abspath(out) == os.getcwd() assert os.path.abspath(wmLoc) == os.path.join(os.getcwd(), 'weather_files') @@ -92,54 +95,55 @@ def test_checkArgs_outloc_1(args): def test_checkArgs_outloc_2(args, tmp_path): '''Tests that the correct output location gets assigned when provided''' with pushd(tmp_path): - args.output_directory = tmp_path + args.runtime_group.output_directory = tmp_path argDict = checkArgs(args) - out = argDict['output_directory'] + out = argDict.runtime_group.output_directory assert out == tmp_path def test_checkArgs_outloc_2b(args, tmp_path): ''' Tests that the weather model directory gets passed through by itself''' with pushd(tmp_path): - args.output_directory = tmp_path - args.weather_model_directory = 'weather_dir' + args.runtime_group.output_directory = tmp_path + wm_dir = Path('weather_dir') + args.runtime_group.weather_model_directory = wm_dir argDict = checkArgs(args) - assert argDict['weather_model_directory'] == 'weather_dir' + assert argDict.runtime_group.weather_model_directory == wm_dir def test_checkArgs_outloc_3(args, tmp_path): '''Tests that the weather model directory gets created when needed''' with pushd(tmp_path): - args.output_directory = tmp_path + args.runtime_group.output_directory = tmp_path argDict = checkArgs(args) - assert os.path.isdir(argDict['weather_model_directory']) + assert argDict.runtime_group.weather_model_directory.is_dir() def test_checkArgs_outloc_4(args): '''Tests for creating writeable weather model directory''' argDict = checkArgs(args) - assert isWriteable(argDict['weather_model_directory']) + assert isWriteable(argDict.runtime_group.weather_model_directory) def test_filenames_1(args): '''tests that the correct filenames are generated''' argDict = checkArgs(args) - assert 'Delay' not in argDict['wetFilenames'][0] - assert 'wet' in argDict['wetFilenames'][0] - assert 'hydro' in argDict['hydroFilenames'][0] - assert '20180101' in argDict['wetFilenames'][0] - assert '20180101' in argDict['hydroFilenames'][0] - assert len(argDict['hydroFilenames']) == 1 + assert 'Delay' not in argDict.wetFilenames[0] + assert 'wet' in argDict.wetFilenames[0] + assert 'hydro' in argDict.hydroFilenames[0] + assert '20180101' in argDict.wetFilenames[0] + assert '20180101' in argDict.hydroFilenames[0] + assert len(argDict.hydroFilenames) == 1 def test_filenames_2(args): '''tests that the correct filenames are generated''' - args['output_directory'] = SCENARIO_2 - args.aoi = StationFile(os.path.join(SCENARIO_2, 'stations.csv')) + args.runtime_group.output_directory = Path(SCENARIO_2) + args.aoi_group.aoi = StationFile(os.path.join(SCENARIO_2, 'stations.csv')) argDict = checkArgs(args) - assert '20180101' in argDict['wetFilenames'][0] - assert len(argDict['wetFilenames']) == 1 + assert '20180101' in argDict.wetFilenames[0] + assert len(argDict.wetFilenames) == 1 def test_makeDelayFileNames_1(): diff --git a/test/test_datelist.py b/test/test_datelist.py index d5baab4e8..8c9859de0 100644 --- a/test/test_datelist.py +++ b/test/test_datelist.py @@ -31,16 +31,16 @@ def test_datelist(): cfg = write_yaml(dct_group, 'temp.yaml') param_dict = read_run_config_file(cfg) - assert param_dict['date_list'] == true_dates + assert param_dict.date_group.date_list == true_dates def test_datestep(): SCENARIO_DIR = os.path.join(TEST_DIR, 'scenario_5') st, en, step = '20200124', '20200130', 3 true_dates = [ - datetime.datetime(2020,1,24), - datetime.datetime(2020,1,27), - datetime.datetime(2020,1,30) + datetime.date(2020,1,24), + datetime.date(2020,1,27), + datetime.date(2020,1,30) ] dct_group = { @@ -56,4 +56,4 @@ def test_datestep(): cfg = write_yaml(dct_group, 'temp.yaml') param_dict = read_run_config_file(cfg) - assert param_dict['date_list'] == true_dates \ No newline at end of file + assert param_dict.date_group.date_list == true_dates \ No newline at end of file diff --git a/test/test_synthetic.py b/test/test_synthetic.py index adb164067..17819e12a 100644 --- a/test/test_synthetic.py +++ b/test/test_synthetic.py @@ -171,10 +171,12 @@ def make_config_dict(self): 'height_group': {'height_levels': self.hgt_lvls.tolist()}, 'time_group': {'time': self.ttime, 'interpolate_time': 'none'}, 'date_group': {'date_list': datetime.strftime(self.dt, '%Y%m%d')}, - 'cube_spacing_in_m': str(self._cube_spacing_m), 'los_group': {'ray_trace': True, 'orbit_file': self.orbit}, 'weather_model': self.wmName, - 'runtime_group': {'output_directory': self.wd}, + 'runtime_group': { + 'output_directory': self.wd, + 'cube_spacing_in_m': str(self._cube_spacing_m), + }, } return dct diff --git a/test/test_util.py b/test/test_util.py index aada388fb..d509bf95b 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -122,7 +122,7 @@ def test_cosd(): def test_rio_open(): - out = rio_open(os.path.join(TEST_DIR, "test_geom", "lat.rdr"), False) + out = rio_open(TEST_DIR / "test_geom/lat.rdr", False) assert np.allclose(out.shape, (45, 226)) @@ -508,9 +508,9 @@ def test_rio_3(): def test_rio_4(): - SCENARIO_DIR = os.path.join(TEST_DIR, "scenario_4") - los = os.path.join(SCENARIO_DIR, 'los.rdr') - inc, hd = rio_open(los, returnProj=False) + SCENARIO_DIR = TEST_DIR / "scenario_4" + los_path = SCENARIO_DIR / 'los.rdr' + inc, hd = rio_open(los_path, returnProj=False) assert len(inc.shape) == 2 assert len(hd.shape) == 2 diff --git a/test/test_validators.py b/test/test_validators.py index 4ccb9a8ca..0137cb11d 100644 --- a/test/test_validators.py +++ b/test/test_validators.py @@ -8,11 +8,11 @@ from test import TEST_DIR -from RAiDER.cli import AttributeDict +from RAiDER.cli.args import DateGroupUnparsed, LOSGroupUnparsed, TimeGroup from RAiDER.cli.validators import ( getBufferedExtent, isOutside, isInside, - enforce_valid_dates as date_type, convert_time as time_type, - enforce_bbox, parse_dates, enforce_wm, get_los + coerce_into_date, + parse_bbox, parse_dates, parse_weather_model, get_los ) SCENARIO = os.path.join(TEST_DIR, "scenario_4") @@ -64,22 +64,23 @@ def args1(): def test_enforce_wm(): with pytest.raises(NotImplementedError): - enforce_wm('notamodel', 'fakeaoi') + parse_weather_model('notamodel', 'fakeaoi') def test_get_los_ray(args1): args = args1 - los = get_los(args) + los_group_unparsed = LOSGroupUnparsed(**args) + los = get_los(los_group_unparsed) assert not los.ray_trace() assert los.is_Projected() def test_date_type(): - assert date_type("2020-10-1") == datetime(2020, 10, 1) - assert date_type("2020101") == datetime(2020, 10, 1) + assert coerce_into_date("2020-10-1") == date(2020, 10, 1) + assert coerce_into_date("2020101") == date(2020, 10, 1) with pytest.raises(ValueError): - date_type("foobar") + coerce_into_date("foobar") @pytest.mark.parametrize("input,expected", ( @@ -98,44 +99,42 @@ def test_date_type(): )) @pytest.mark.parametrize("timezone", ("", "z", "+0000")) def test_time_type(input, timezone, expected): - assert time_type(input + timezone) == expected + assert TimeGroup.coerce_into_time(input + timezone) == expected def test_time_type_error(): with pytest.raises(ValueError): - time_type("foobar") + TimeGroup.coerce_into_time("foobar") def test_date_list_action(): - date_list = { - 'date_start':'20200101', - } - assert date_type(date_list['date_start']) == datetime(2020,1,1) - - - assert parse_dates(date_list) == [datetime(2020,1,1)] + date_group_unparsed = DateGroupUnparsed( + date_start='20200101', + ) + assert coerce_into_date(date_group_unparsed.date_start) == date(2020,1,1) + assert parse_dates(date_group_unparsed).date_list == [date(2020,1,1)] - date_list['date_end'] = '20200103' - assert date_type(date_list['date_end']) == datetime(2020,1,3) - assert parse_dates(date_list) == [datetime(2020,1,1), datetime(2020,1,2), datetime(2020,1,3)] + date_group_unparsed.date_end = '20200103' + assert coerce_into_date(date_group_unparsed.date_end) == date(2020,1,3) + assert parse_dates(date_group_unparsed).date_list == [date(2020,1,1), date(2020,1,2), date(2020,1,3)] - date_list['date_end'] = '20200112' - date_list['date_step'] = '5' - assert parse_dates(date_list) == [datetime(2020,1,1), datetime(2020,1,6), datetime(2020,1,11)] + date_group_unparsed.date_end = '20200112' + date_group_unparsed.date_step = '5' + assert parse_dates(date_group_unparsed).date_list == [date(2020,1,1), date(2020,1,6), date(2020,1,11)] def test_bbox_action(): bbox_str = "45 46 -72 -70" - assert len(enforce_bbox(bbox_str)) == 4 + assert len(parse_bbox(bbox_str)) == 4 - assert enforce_bbox(bbox_str) == [45, 46, -72, -70] + assert parse_bbox(bbox_str) == (45, 46, -72, -70) with pytest.raises(ValueError): - enforce_bbox("20 20 30 30") + parse_bbox("20 20 30 30") with pytest.raises(ValueError): - enforce_bbox("30 100 20 40") + parse_bbox("30 100 20 40") with pytest.raises(ValueError): - enforce_bbox("10 30 40 190") + parse_bbox("10 30 40 190") def test_ll1(llsimple): @@ -159,13 +158,18 @@ def test_ll4(llarray): def test_isOutside1(llsimple): - assert isOutside(getBufferedExtent(*llsimple), getBufferedExtent(*llsimple) + 1) + extent1 = getBufferedExtent(*llsimple) + extent2 = extent1[0] + 1, extent1[1] + 1, extent1[2] + 1, extent1[3] + 1 + assert isOutside(extent1, extent2) def test_isOutside2(llsimple): - assert not isOutside(getBufferedExtent(*llsimple), getBufferedExtent(*llsimple)) + extent = getBufferedExtent(*llsimple) + assert not isOutside(extent, extent) def test_isInside(llsimple): - assert isInside(getBufferedExtent(*llsimple), getBufferedExtent(*llsimple)) - assert not isInside(getBufferedExtent(*llsimple), getBufferedExtent(*llsimple) + 1) + extent1 = getBufferedExtent(*llsimple) + extent2 = extent1[0] + 1, extent1[1] + 1, extent1[2] + 1, extent1[3] + 1 + assert isInside(extent1, extent1) + assert not isInside(extent1, extent2) diff --git a/test/test_weather_model.py b/test/test_weather_model.py index a403c0710..bd6c0db81 100644 --- a/test/test_weather_model.py +++ b/test/test_weather_model.py @@ -319,7 +319,7 @@ def test_hrrr(hrrr: HRRR): wm.checkValidBounds(np.array([35, 40, -95, -90])) with pytest.raises(ValueError): - wm.checkValidBounds([45, 47, 300, 310]) + wm.checkValidBounds(np.array([45, 47, 300, 310])) def test_hrrrak(hrrrak: HRRRAK): @@ -331,7 +331,7 @@ def test_hrrrak(hrrrak: HRRRAK): wm.checkValidBounds(np.array([45, 47, 200, 210])) with pytest.raises(ValueError): - wm.checkValidBounds([15, 20, 265, 270]) + wm.checkValidBounds(np.array([15, 20, 265, 270])) with pytest.raises(DatetimeOutsideRange): wm.checkTime(datetime.datetime(2018, 7, 12).replace(tzinfo=datetime.timezone(offset=datetime.timedelta()))) diff --git a/tools/RAiDER/aria/prepFromGUNW.py b/tools/RAiDER/aria/prepFromGUNW.py index 34bbe0e6f..65a9c27f4 100644 --- a/tools/RAiDER/aria/prepFromGUNW.py +++ b/tools/RAiDER/aria/prepFromGUNW.py @@ -365,7 +365,6 @@ def main(args: CalcDelaysArgs) -> tuple[Path, float]: raider_cfg = { 'weather_model': args.weather_model, 'look_dir': GUNWObj.look_dir, - 'cube_spacing_in_m': GUNWObj.spacing_m, 'aoi_group': {'bounding_box': GUNWObj.SNWE}, 'height_group': {'height_levels': GUNWObj.heights}, 'date_group': {'date_list': GUNWObj.dates}, @@ -377,11 +376,11 @@ def main(args: CalcDelaysArgs) -> tuple[Path, float]: 'los_group': { 'ray_trace': True, 'orbit_file': GUNWObj.orbit_file, - 'wavelength': GUNWObj.wavelength, }, 'runtime_group': { 'raster_format': 'nc', 'output_directory': args.output_directory, + 'cube_spacing_in_m': GUNWObj.spacing_m, }, } diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index 3405b0e11..5397681f0 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -5,94 +5,95 @@ # RESERVED. United States Government Sponsorship acknowledged. # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -import os -from datetime import datetime +import datetime as dt from pathlib import Path from typing import Optional import pandas as pd import rasterio.drivers as rd +from RAiDER.cli.args import RunConfig from RAiDER.llreader import BoundingBox, StationFile from RAiDER.logger import logger from RAiDER.losreader import LOS, Zenith -def checkArgs(args): - """ - Check argument compatibility and return the correct variables. - """ +def checkArgs(run_config: RunConfig) -> RunConfig: + """Check argument compatibility and return the correct variables.""" + ############################################################################ # Directories - if args.weather_model_directory is None: - args.weather_model_directory = os.path.join(args.output_directory, 'weather_files') - - os.makedirs(args.output_directory, exist_ok=True) - os.makedirs(args.weather_model_directory, exist_ok=True) - args['weather_model'].set_wmLoc(args.weather_model_directory) + run_config.runtime_group.output_directory.mkdir(exist_ok=True) + run_config.runtime_group.weather_model_directory.mkdir(exist_ok=True) + run_config.weather_model.set_wmLoc(str(run_config.runtime_group.weather_model_directory)) - ######################################################################################################################### + ############################################################################ # Date and Time parsing - args.date_list = [datetime.combine(d, args.time) for d in args.date_list] - if (len(args.date_list) > 1) & (args.orbit_file is not None): + run_config.date_group.date_list = [ + dt.datetime.combine(d, run_config.time_group.time) + for d in run_config.date_group.date_list + ] + if len(run_config.date_group.date_list) > 1 and run_config.los_group.orbit_file is not None: logger.warning( - 'Only one orbit file is being used to get the look vectors for all requested times, if you want to use ' - 'separate orbit files you will need to run raider separately for each time.' + 'Only one orbit file is being used to get the look vectors for all requested times. If you want to use ' + 'separate orbit files you will need to run RAiDER separately for each time.' ) - args.los.setTime(args.date_list[0]) + run_config.los_group.los.setTime(run_config.date_group.date_list[0]) - ######################################################################################################################### + ############################################################################ # filenames - wetNames, hydroNames = [], [] - for d in args.date_list: - if args.aoi.type() != 'bounding_box': + wetNames: list[str] = [] + hydroNames: list[str] = [] + for d in run_config.date_group.date_list: + if not isinstance(run_config.aoi_group.aoi, BoundingBox): # Handle the GNSS station file - if args.aoi.type() == 'station_file': + if isinstance(run_config.aoi_group.aoi, StationFile): wetFilename = str( - args.output_directory / - f'{args.weather_model._dataset.upper()}_Delay_{d.strftime("%Y%m%dT%H%M%S")}_ztd.csv' + run_config.runtime_group.output_directory / + f'{run_config.weather_model._dataset.upper()}_Delay_{d.strftime("%Y%m%dT%H%M%S")}_ztd.csv' ) hydroFilename = '' # only the 'wetFilename' is used for the station_file # copy the input station file to the output location for editing - indf = pd.read_csv(args.aoi._filename).drop_duplicates(subset=['Lat', 'Lon']) + indf = pd.read_csv(run_config.aoi_group.aoi._filename) \ + .drop_duplicates(subset=['Lat', 'Lon']) indf.to_csv(wetFilename, index=False) else: # This implies rasters - fmt = get_raster_ext(args.file_format) + fmt = get_raster_ext(run_config.runtime_group.file_format) wetFilename, hydroFilename = makeDelayFileNames( d, - args.los, + run_config.los_group.los, fmt, - args.weather_model._dataset.upper(), - args.output_directory, + run_config.weather_model._dataset.upper(), + run_config.runtime_group.output_directory, ) else: # In this case a cube file format is needed - if args.file_format not in '.nc .h5 h5 hdf5 .hdf5 nc'.split(): + if run_config.runtime_group.file_format not in '.nc .h5 h5 hdf5 .hdf5 nc'.split(): fmt = 'nc' - logger.debug('Invalid extension %s for cube. Defaulting to .nc', args.file_format) + logger.debug('Invalid extension %s for cube. Defaulting to .nc', run_config.runtime_group.file_format) else: - fmt = args.file_format.strip('.').replace('df', '') + fmt = run_config.runtime_group.file_format.strip('.').replace('df', '') wetFilename, hydroFilename = makeDelayFileNames( d, - args.los, + run_config.los_group.los, fmt, - args.weather_model._dataset.upper(), - args.output_directory, + run_config.weather_model._dataset.upper(), + run_config.runtime_group.output_directory, ) wetNames.append(wetFilename) hydroNames.append(hydroFilename) - args.wetFilenames = wetNames - args.hydroFilenames = hydroNames + run_config.wetFilenames = wetNames + run_config.hydroFilenames = hydroNames - return args + return run_config def get_raster_ext(fmt): @@ -109,7 +110,7 @@ def get_raster_ext(fmt): raise ValueError(f'{fmt} is not a valid gdal/rasterio file format for rasters') -def makeDelayFileNames(time: Optional[datetime], los: Optional[LOS], outformat: str, weather_model_name: str, out: Path) -> tuple[str, str]: +def makeDelayFileNames(date: Optional[dt.date], los: Optional[LOS], outformat: str, weather_model_name: str, out: Path) -> tuple[str, str]: """ return names for the wet and hydrostatic delays. @@ -121,7 +122,7 @@ def makeDelayFileNames(time: Optional[datetime], los: Optional[LOS], outformat: """ format_string = '{model_name}_{{}}_{time}{los}.{ext}'.format( model_name=weather_model_name, - time=time.strftime('%Y%m%dT%H%M%S_') if time is not None else '', + time=date.strftime('%Y%m%dT%H%M%S_') if date is not None else '', los='ztd' if (isinstance(los, Zenith) or los is None) else 'std', ext=outformat, ) diff --git a/tools/RAiDER/cli/__init__.py b/tools/RAiDER/cli/__init__.py index b1dd22c39..e69de29bb 100644 --- a/tools/RAiDER/cli/__init__.py +++ b/tools/RAiDER/cli/__init__.py @@ -1,45 +0,0 @@ -from RAiDER.constants import _CUBE_SPACING_IN_M, _ZREF - - -class AttributeDict(dict): - __getattr__ = dict.__getitem__ - __setattr__ = dict.__setitem__ - __delattr__ = dict.__delitem__ - - -DEFAULT_DICT = AttributeDict( - dict( - look_dir='right', - date_start=None, - date_end=None, - date_step=None, - date_list=None, - time=None, - end_time=None, - weather_model=None, - lat_file=None, - lon_file=None, - station_file=None, - bounding_box=None, - geocoded_file=None, - dem=None, - use_dem_latlon=False, - height_levels=None, - height_file_rdr=None, - ray_trace=False, - zref=_ZREF, - cube_spacing_in_m=_CUBE_SPACING_IN_M, - los_file=None, - los_convention='isce', - los_cube=None, - orbit_file=None, - verbose=True, - raster_format='GTiff', - file_format='GTiff', - download_only=False, - output_directory='.', - weather_model_directory=None, - output_projection='EPSG:4326', - interpolate_time='center_time', - ) -) diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 9468c6af7..0a704eaff 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -16,7 +16,18 @@ import RAiDER.aria.calcGUNW import RAiDER.aria.prepFromGUNW from RAiDER import aws -from RAiDER.cli import DEFAULT_DICT, AttributeDict +from RAiDER.cli.args import ( + AOIGroup, + AOIGroupUnparsed, + DateGroupUnparsed, + HeightGroupUnparsed, + LOSGroup, + LOSGroupUnparsed, + RAiDERArgs, + RunConfig, + RuntimeGroup, + TimeGroup, +) from RAiDER.cli.parser import add_cpus, add_out, add_verbose from RAiDER.cli.validators import DateListAction, date_type from RAiDER.logger import logger, logging @@ -51,20 +62,26 @@ DEFAULT_RUN_CONFIG_PATH = Path('./examples/template/template.yaml') -def read_run_config_file(path: Path) -> AttributeDict: +def read_run_config_file(path: Path) -> RunConfig: """ Read the run config file into a dictionary structure. Args: path (Path): path to the run config file Returns: - dict: arguments to pass to RAiDER functions + RAiDERArgs: arguments to pass to RAiDER functions Examples: >>> run_config = read_run_config_file('raider.yaml') """ - from RAiDER.cli.validators import enforce_time, enforce_wm, get_heights, get_los, get_query_region, parse_dates + from RAiDER.cli.validators import ( + get_heights, + get_los, + get_query_region, + parse_dates, + parse_weather_model, + ) with path.open() as f: try: @@ -82,54 +99,46 @@ def read_run_config_file(path: Path) -> AttributeDict: if not isinstance(yaml_data['look_dir'], str) or yaml_data['look_dir'].lower() not in ('right', 'left'): raise ValueError(f'Unknown look direction {yaml_data['look_dir']}') - # Parse the user-provided arguments - run_config = DEFAULT_DICT.copy() - for key, value in yaml_data.items(): - if key == 'runtime_group': - for k, v in value.items(): - if v is not None: - run_config[k] = v - if key == 'time_group': - run_config.update(enforce_time(AttributeDict(value))) - if key == 'date_group': - run_config['date_list'] = parse_dates(AttributeDict(value)) - if key == 'aoi_group': - # in case a DEM is passed and should be used - dct_temp = {**AttributeDict(value), **AttributeDict(yaml_data['height_group'])} - run_config['aoi'] = get_query_region(AttributeDict(dct_temp)) - - if key == 'los_group': - run_config['los'] = get_los(AttributeDict(value)) - run_config['zref'] = AttributeDict(value).get('zref') - if key == 'look_dir': - if value.lower() not in ['right', 'left']: - raise ValueError(f'Unknown look direction {value}') - run_config['look_dir'] = value.lower() - if key == 'cube_spacing_in_m': - run_config[key] = float(value) if isinstance(value, str) else value - if key == 'download_only': - run_config[key] = bool(value) - - # Have to guarantee that certain variables exist prior to looking at heights - for key, value in yaml_data.items(): - if key == 'height_group': - run_config.update( - get_heights( - AttributeDict(value), - run_config['output_directory'], - run_config['station_file'], - run_config['bounding_box'], - ) - ) + # Support for deprecated location for cube_spacing_in_m + if 'cube_spacing_in_m' in yaml_data: + logger.warning( + 'Run config option cube_spacing_in_m is deprecated. Please use runtime_group.cube_spacing_in_m instead.' + ) + yaml_data['runtime_group']['cube_spacing_in_m'] = yaml_data['cube_spacing_in_m'] - if key == 'weather_model': - run_config[key] = enforce_wm(value, run_config['aoi']) + # Parse the user-provided arguments + height_group_unparsed = HeightGroupUnparsed(**yaml_data['height_group']) + aoi_group_unparsed = AOIGroupUnparsed(**yaml_data['aoi_group']) + runtime_group = RuntimeGroup(**yaml_data['runtime_group']) + aoi_group = AOIGroup( + aoi=get_query_region( + aoi_group_unparsed, + height_group_unparsed + ) + ) - run_config['aoi']._cube_spacing_m = run_config['cube_spacing_in_m'] - return AttributeDict(run_config) + return RunConfig( + look_dir=yaml_data['look_dir'].lower(), + weather_model=parse_weather_model(yaml_data['weather_model'], aoi_group.aoi), + date_group=parse_dates(DateGroupUnparsed(**yaml_data['date_group'])), + time_group=TimeGroup(**yaml_data['time_group']), + aoi_group=aoi_group, + height_group=get_heights( + height_group=height_group_unparsed, + aoi_group=aoi_group_unparsed, + runtime_group=runtime_group, + ), + los_group=LOSGroup( + los=get_los(LOSGroupUnparsed(**yaml_data['los_group'])), + **yaml_data['los_group'] + ), + runtime_group=runtime_group, + ) def drop_nans(d: dict[str, Any]) -> dict[str, Any]: + # Must iterate over a copy of the dict's keys because dict.keys() cannot + # be used directly when the dict's size is going to change. for key in list(d.keys()): if d[key] is None: del d[key] @@ -161,6 +170,7 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: p.add_argument( '--download_only', action='store_true', + default=False, help='only download a weather model.' ) @@ -187,7 +197,7 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: ) # if not None, will replace first argument (run_config_file) - args = p.parse_args(args=iargs) + args: RAiDERArgs = p.parse_args(args=iargs, namespace=RAiDERArgs()) # Default example run configuration file ex_run_config_name = args.generate_config or 'template' @@ -203,7 +213,6 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: shutil.copy(filename, str(Path.cwd())) logger.info('Wrote: %s', filename) sys.exit() - # args.generate_config now guaranteed to be None # If no run configuration file is provided, look for a ./raider.yaml if args.run_config_file is not None: @@ -221,28 +230,28 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: args.run_config_file = DEFAULT_RUN_CONFIG_PATH # Read the run config file - params = read_run_config_file(args.run_config_file) + run_config = read_run_config_file(args.run_config_file) # Verify the run config file's parameters - params = checkArgs(params) - dl_only = params['download_only'] or args.download_only + run_config = checkArgs(run_config) + dl_only = run_config.runtime_group.download_only or args.download_only - if not params.verbose: + if not run_config.runtime_group.verbose: logger.setLevel(logging.INFO) # Extract and buffer the AOI - los = params['los'] - aoi = params['aoi'] - model = params['weather_model'] + los = run_config.los_group.los + aoi = run_config.aoi_group.aoi + model = run_config.weather_model # adjust user requested AOI by grid size and buffer slightly aoi.add_buffer(model.getLLRes()) # define the xy grid within the buffered bounding box - aoi.set_output_xygrid(params['output_projection']) + aoi.set_output_xygrid(run_config.runtime_group.output_projection) # add a buffer determined by latitude for ray tracing - if los.ray_trace(): + if isinstance(los, Raytracing): wm_bounds = aoi.calc_buffer_ray(los.getSensorDirection(), lookDir=los.getLookDirection(), incAngle=30) else: wm_bounds = aoi.bounds() @@ -250,7 +259,10 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: model.set_latlon_bounds(wm_bounds, output_spacing=aoi.get_output_spacing()) wet_paths: list[Path] = [] - for t, w, f in zip(params['date_list'], params['wetFilenames'], params['hydroFilenames']): + t: datetime.datetime + w: str + f: str + for t, w, f in zip(run_config.date_group.date_list, run_config.wetFilenames, run_config.hydroFilenames): ########################################################### # Weather model calculation ########################################################### @@ -258,7 +270,7 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: logger.debug(f'Requested date,time: {t.strftime("%Y%m%d, %H:%M")}') logger.debug('Beginning weather model pre-processing') - interp_method = params.get('interpolate_time') + interp_method = run_config.time_group.interpolate_time if interp_method is None: interp_method = 'none' logger.warning( @@ -284,11 +296,16 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: wfiles = [] for tt in times: try: - wfile = RAiDER.processWM.prepareWeatherModel(model, tt, aoi.bounds(), makePlots=params['verbose']) + wfile = RAiDER.processWM.prepareWeatherModel( + model, + tt, + aoi.bounds(), + makePlots=run_config.runtime_group.verbose + ) wfiles.append(wfile) except TryToKeepGoingError: - if interp_method in ['azimuth_time_grid', 'none']: + if interp_method in ('azimuth_time_grid', 'none'): raise DatetimeFailed(model.Model(), tt) else: continue @@ -317,9 +334,9 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: weather_model_file, aoi, los, - height_levels=params['height_levels'], - out_proj=params['output_projection'], - zref=params['zref'], + height_levels=run_config.height_group.height_levels, + out_proj=run_config.runtime_group.output_projection, + zref=run_config.los_group.zref, ) except RuntimeError: logger.exception('Datetime %s failed', t) @@ -345,8 +362,8 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: # data provenance: include metadata for model and times used times_str = [t.strftime('%Y%m%dT%H:%M:%S') for t in sorted(times)] ds = ds.assign_attrs(model_name=model._Name, model_times_used=times_str, interpolation_method=interp_method) - if ext not in ['.nc', '.h5']: - out_filename = out_filename.stem + '.nc' + if ext not in ('.nc', '.h5'): + out_filename = Path(out_filename.stem + '.nc') if out_filename.suffix == '.nc': ds.to_netcdf(out_filename, mode='w') @@ -358,7 +375,7 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: else: out_filename = Path(out_filename) if aoi.type() == 'station_file': - out_filename = out_filename.stem + '.csv' + out_filename = Path(out_filename.stem + '.csv') if aoi.type() in ('station_file', 'radar_rasters', 'geocoded_file'): writeDelays(aoi, wet_delay, hydro_delay, str(out_filename), f, outformat=run_config.runtime_group.raster_format) @@ -505,7 +522,8 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> Optional[xr.Dataset]: p.add_argument( '-f', '--file', - help='1 ARIA GUNW netcdf file' + type=lambda p: Path(p).absolute(), + help='1 ARIA GUNW netcdf file', ) p.add_argument( @@ -537,7 +555,7 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> Optional[xr.Dataset]: choices=TIME_INTERPOLATION_METHODS, help=( 'How to interpolate across model time steps. Possible options are: ' - "['none', 'center_time', 'azimuth_time_grid'] " + f"{TIME_INTERPOLATION_METHODS} " 'None: means nearest model time; center_time: linearly across center time; ' 'Azimuth_time_grid: means every pixel is weighted with respect to azimuth time of S1' ), @@ -556,10 +574,6 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> Optional[xr.Dataset]: if args.input_bucket_prefix is None: args.input_bucket_prefix = args.bucket_prefix - if args.interpolate_time not in TIME_INTERPOLATION_METHODS: - raise ValueError("interpolate_time arg must be in ['none', 'center_time', 'azimuth_time_grid']") - args.interpolate_time = cast(TimeInterpolationMethod, args.interpolate_time) - if args.weather_model == 'None': # NOTE: HyP3's current step function implementation does not have a good way of conditionally # running processing steps. This allows HyP3 to always run this step but exit immediately @@ -578,11 +592,15 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> Optional[xr.Dataset]: raise NoWeatherModelData('The required HRRR data for time-grid interpolation is not available') if args.file is None: - if args.bucket is None: + if args.bucket is None or args.bucket_prefix is None: raise ValueError('Either argument --file or --bucket must be provided') # only use GUNW ID for checking if HRRR available - args.file = aws.get_s3_file(args.bucket, args.input_bucket_prefix, '.nc') + args.file = aws.get_s3_file( + args.bucket, + cast(str, args.input_bucket_prefix), # guaranteed not None at this point + '.nc' + ) if args.file is None: raise ValueError( 'GUNW product file could not be found at' f's3://{args.bucket}/{args.input_bucket_prefix}' @@ -602,7 +620,11 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> Optional[xr.Dataset]: # we include this within this portion of the control flow. print('Nothing to do because outside of weather model range') return - json_file_path = aws.get_s3_file(args.bucket, args.input_bucket_prefix, '.json') + json_file_path = aws.get_s3_file( + args.bucket, + cast(str, args.input_bucket_prefix), + '.json' + ) if json_file_path is None: raise ValueError( 'GUNW metadata file could not be found at' f's3://{args.bucket}/{args.input_bucket_prefix}' diff --git a/tools/RAiDER/cli/validators.py b/tools/RAiDER/cli/validators.py index 5c5097527..b1ad4544c 100755 --- a/tools/RAiDER/cli/validators.py +++ b/tools/RAiDER/cli/validators.py @@ -1,19 +1,28 @@ +import argparse +import datetime as dt import importlib import itertools import re -from argparse import Action, ArgumentError, ArgumentTypeError -from datetime import date, datetime, time, timedelta -from textwrap import dedent -from time import strptime +import time from pathlib import Path from typing import Any, Optional, Self, Union import numpy as np import pandas as pd -from RAiDER.llreader import BoundingBox, GeocodedFile, Geocube, RasterRDR, StationFile +from RAiDER.cli.args import ( + AOIGroup, + AOIGroupUnparsed, + DateGroup, + DateGroupUnparsed, + HeightGroup, + HeightGroupUnparsed, + LOSGroupUnparsed, + RuntimeGroup, +) +from RAiDER.llreader import AOI, BoundingBox, GeocodedFile, Geocube, RasterRDR, StationFile from RAiDER.logger import logger -from RAiDER.losreader import Conventional, Zenith +from RAiDER.losreader import LOS, Conventional, Zenith from RAiDER.models.weatherModel import WeatherModel from RAiDER.types import BB from RAiDER.utilFcns import rio_extents, rio_profile @@ -22,145 +31,146 @@ _BUFFER_SIZE = 0.2 # default buffer size in lat/lon degrees -def enforce_wm(value, aoi): - model = value.upper().replace('-', '') +def parse_weather_model(weather_model_name: str, aoi: AOI) -> WeatherModel: + weather_model_name = weather_model_name.upper().replace('-', '') try: - _, model_obj = modelName2Module(model) + _, Model = get_wm_by_name(weather_model_name) except ModuleNotFoundError: raise NotImplementedError( - dedent(f""" - Model {model} is not yet fully implemented, - please contribute! - """) + f'Model {weather_model_name} is not yet fully implemented, please contribute!' ) - # check the user requsted bounding box is within the weather model domain - modObj = model_obj().checkValidBounds(aoi.bounds()) + # Check that the user-requested bounding box is within the weather model domain + model: WeatherModel = Model() + model.checkValidBounds(aoi.bounds()) - return modObj + return model -def get_los(args): - if args.get('orbit_file'): - if args.get('ray_trace'): +def get_los(los_group: LOSGroupUnparsed) -> LOS: + if los_group.orbit_file is not None: + if los_group.ray_trace: from RAiDER.losreader import Raytracing - - los = Raytracing(args.orbit_file) + los = Raytracing(los_group.orbit_file) else: - los = Conventional(args.orbit_file) - elif args.get('los_file'): - if args.ray_trace: - from RAiDER.losreader import Raytracing + los = Conventional(los_group.orbit_file) - los = Raytracing(args.los_file, args.los_convention) + elif los_group.los_file is not None: + if los_group.ray_trace: + from RAiDER.losreader import Raytracing + los = Raytracing(los_group.los_file, los_group.los_convention) else: - los = Conventional(args.los_file, args.los_convention) + los = Conventional(los_group.los_file, los_group.los_convention) - elif args.get('los_cube'): + elif los_group.los_cube is not None: raise NotImplementedError('LOS_cube is not yet implemented') - # if args.ray_trace: - # los = Raytracing(args.los_cube) - # else: - # los = Conventional(args.los_cube) + # if los_group.ray_trace: + # los = Raytracing(los_group.los_cube) + # else: + # los = Conventional(los_group.los_cube) else: los = Zenith() return los -def get_heights(args, out, station_file, bounding_box=None): +def get_heights(height_group: HeightGroupUnparsed, aoi_group: AOIGroupUnparsed, runtime_group: RuntimeGroup) -> HeightGroup: """Parse the Height info and download a DEM if needed.""" - dem_path = out - - out = { - 'dem': args.get('dem'), - 'height_file_rdr': None, - 'height_levels': None, - } - - if args.get('dem'): - if station_file is not None: - if 'Hgt_m' not in pd.read_csv(station_file): - out['dem'] = os.path.join(dem_path, 'GLO30.dem') - elif os.path.exists(args.dem): - out['dem'] = args.dem + result = HeightGroup( + dem=height_group.dem, + use_dem_latlon=height_group.use_dem_latlon, + height_file_rdr=height_group.height_file_rdr, + height_levels=None, + ) + + if height_group.dem is not None: + if aoi_group.station_file is not None: + station_data = pd.read_csv(aoi_group.station_file) + if 'Hgt_m' not in station_data: + result.dem = runtime_group.output_directory / 'GLO30.dem' + elif Path(height_group.dem).exists(): # crop the DEM - if bounding_box is not None: - dem_bounds = rio_extents(rio_profile(args.dem)) - lats = dem_bounds[:2] - lons = dem_bounds[2:] + if aoi_group.bounding_box is not None: + dem_bounds = rio_extents(rio_profile(height_group.dem)) + lats: BB.SN = dem_bounds[:2] + lons: BB.WE = dem_bounds[2:] if isOutside( - bounding_box, + parse_bbox(aoi_group.bounding_box), getBufferedExtent( lats, lons, - buf=_BUFFER_SIZE, + buffer_size=_BUFFER_SIZE, ), ): raise ValueError( 'Existing DEM does not cover the area of the input lat/lon points; either move the DEM, delete ' 'it, or change the input points.' ) - else: - pass # will download the dem later + # else: will download the dem later - elif args.get('height_file_rdr'): - out['height_file_rdr'] = args.height_file_rdr - - else: + elif height_group.height_file_rdr is None: # download the DEM if needed - out['dem'] = os.path.join(dem_path, 'GLO30.dem') + result.dem = runtime_group.output_directory / 'GLO30.dem' - if args.get('height_levels'): - if isinstance(args.height_levels, str): - l = re.findall('[-0-9]+', args.height_levels) + if height_group.height_levels is not None: + if isinstance(height_group.height_levels, str): + levels = re.findall('[-0-9]+', height_group.height_levels) else: - l = args.height_levels + levels = height_group.height_levels - out['height_levels'] = np.array([float(ll) for ll in l]) - if np.any(out['height_levels'] < 0): + levels = np.array([float(level) for level in levels]) + if np.any(levels < 0): logger.warning( 'Weather model only extends to the surface topography; ' 'height levels below the topography will be interpolated from the surface and may be inaccurate.' ) + result.height_levels = list(levels) - return out + return result -def get_query_region(args): - """Parse the query region from inputs.""" +def get_query_region(aoi_group: AOIGroupUnparsed, height_group: HeightGroupUnparsed) -> AOI: + """Parse the query region from inputs. + + This function determines the query region from the input parameters. It will return an AOI object that can be used + to query the weather model. + Note: both an AOI group and a height group are necessary in case a DEM is needed. + """ # Get bounds from the inputs # make sure this is first - if args.get('use_dem_latlon'): - query = GeocodedFile(args.dem, is_dem=True) - - elif args.get('lat_file'): - hgt_file = args.get('height_file_rdr') # only get it if exists - dem_file = args.get('dem') - query = RasterRDR(args.lat_file, args.lon_file, hgt_file, dem_file) + if height_group.use_dem_latlon: + query = GeocodedFile(height_group.dem, is_dem=True) + + elif aoi_group.lat_file is not None or aoi_group.lon_file is not None: + if aoi_group.lat_file is None or aoi_group.lon_file is None: + raise ValueError('A lon_file must be specified if a lat_file is specified') + query = RasterRDR( + aoi_group.lat_file, aoi_group.lon_file, + height_group.height_file_rdr, height_group.dem + ) - elif args.get('station_file'): - query = StationFile(args.station_file) + elif aoi_group.station_file is not None: + query = StationFile(aoi_group.station_file) - elif args.get('bounding_box'): - bbox = enforce_bbox(args.bounding_box) - if (np.min(bbox[0]) < -90) | (np.max(bbox[1]) > 90): + elif aoi_group.bounding_box is not None: + bbox = parse_bbox(aoi_group.bounding_box) + if np.min(bbox[0]) < -90 or np.max(bbox[1]) > 90: raise ValueError('Lats are out of N/S bounds; are your lat/lon coordinates switched? Should be SNWE') query = BoundingBox(bbox) - elif args.get('geocoded_file'): - gfile = os.path.basename(args.geocoded_file).upper() - if gfile.startswith('SRTM') or gfile.startswith('GLO'): - logger.debug('Using user DEM: %s', gfile) + elif aoi_group.geocoded_file is not None: + filename = Path(aoi_group.geocoded_file).name.upper() + if filename.startswith('SRTM') or filename.startswith('GLO'): + logger.debug('Using user DEM: %s', filename) is_dem = True else: is_dem = False - query = GeocodedFile(args.geocoded_file, is_dem=is_dem) + query = GeocodedFile(aoi_group.geocoded_file, is_dem=is_dem) # untested - elif args.get('geo_cube'): - query = Geocube(args.geo_cube) + elif aoi_group.geo_cube is not None: + query = Geocube(aoi_group.geo_cube) else: # TODO: Need to incorporate the cube @@ -169,8 +179,8 @@ def get_query_region(args): return query -def enforce_bbox(bbox): - """Enforce a valid bounding box.""" +def parse_bbox(bbox: Union[str, list[Union[int, float]], tuple]) -> BB.SNWE: + """Parse a bounding box string input and ensure it is valid.""" if isinstance(bbox, str): bbox = [float(d) for d in bbox.strip().split()] else: @@ -194,43 +204,47 @@ def enforce_bbox(bbox): 'Lons are out of W/E bounds (-180 to 180); Lons in the format of (0 to 360) are not supported.' ) - return bbox + return S, N, W, E -def parse_dates(arg_dict): +def parse_dates(date_group: DateGroupUnparsed) -> DateGroup: """Determine the requested dates from the input parameters.""" - if arg_dict.get('date_list'): - l = arg_dict['date_list'] - if isinstance(l, str): - l = re.findall('[0-9]+', l) - elif isinstance(l, int): - l = [l] - L = [enforce_valid_dates(d) for d in l] + if date_group.date_list is not None: + if isinstance(date_group.date_list, str): + unparsed_dates = re.findall('[0-9]+', date_group.date_list) + elif isinstance(date_group.date_list, int): + unparsed_dates = [date_group.date_list] + else: + unparsed_dates = date_group.date_list + date_list = [coerce_into_date(d) for d in unparsed_dates] else: - try: - start = arg_dict['date_start'] - except KeyError: + if date_group.date_start is None: raise ValueError('Inputs must include either date_list or date_start') - start = enforce_valid_dates(start) + start = coerce_into_date(date_group.date_start) - if arg_dict.get('date_end'): - end = arg_dict['date_end'] - end = enforce_valid_dates(end) + if date_group.date_end is not None: + end = coerce_into_date(date_group.date_end) else: end = start - if arg_dict.get('date_step'): - step = int(arg_dict['date_step']) + if date_group.date_step: + step = int(date_group.date_step) else: step = 1 - L = [start + timedelta(days=step) for step in range(0, (end - start).days + 1, step)] + date_list = [ + start + dt.timedelta(days=step) + for step in range(0, (end - start).days + 1, step) + ] + + return DateGroup( + date_list=date_list, + ) - return L -def enforce_valid_dates(arg): +def coerce_into_date(val: Union[int, str]) -> dt.date: """Parse a date from a string in pseudo-ISO 8601 format.""" year_formats = ( '%Y-%m-%d', @@ -241,54 +255,11 @@ def enforce_valid_dates(arg): for yf in year_formats: try: - return datetime.strptime(str(arg), yf) - except ValueError: - pass - - raise ValueError(f'Unable to coerce {arg} to a date. Try %Y-%m-%d') - - -def enforce_time(arg_dict): - """Parse an input time (required to be ISO 8601).""" - try: - arg_dict['time'] = convert_time(arg_dict['time']) - except KeyError: - raise ValueError('You must specify a "time" in the input config file') - - if 'end_time' in arg_dict.keys(): - arg_dict['end_time'] = convert_time(arg_dict['end_time']) - return arg_dict - - -def convert_time(inp): - time_formats = ( - '', - 'T%H:%M:%S.%f', - 'T%H%M%S.%f', - '%H%M%S.%f', - 'T%H:%M:%S', - '%H:%M:%S', - 'T%H%M%S', - '%H%M%S', - 'T%H:%M', - 'T%H%M', - '%H:%M', - 'T%H', - ) - timezone_formats = ( - '', - 'Z', - '%z', - ) - all_formats = map(''.join, itertools.product(time_formats, timezone_formats)) - - for tf in all_formats: - try: - return time(*strptime(inp, tf)[3:6]) + return dt.datetime.strptime(str(val), yf).date() except ValueError: pass - raise ValueError(f'Unable to coerce "{inp}" to a time. Try T%H:%M:%S') + raise ValueError(f'Unable to coerce {val} to a date. Try %Y-%m-%d') def get_wm_by_name(model_name: str) -> tuple[str, WeatherModel]: @@ -296,7 +267,7 @@ def get_wm_by_name(model_name: str) -> tuple[str, WeatherModel]: Turn an arbitrary string into a module name. Takes as input a model name, which hopefully looks like ERA-I, and - converts it to a module name, which will look like erai. I doesn't + converts it to a module name, which will look like erai. It doesn't always produce a valid module name, but that's not the goal. The goal is just to handle common cases. Inputs: @@ -311,77 +282,53 @@ def get_wm_by_name(model_name: str) -> tuple[str, WeatherModel]: return module_name, Model -def getBufferedExtent(lats, lons=None, buf=0.0): +def getBufferedExtent(lats: BB.SN, lons: BB.WE, buffer_size: float=0.0) -> BB.SNWE: """Get the bounding box around a set of lats/lons.""" - if lons is None: - lats, lons = lats[..., 0], lons[..., 1] + return ( + min(lats) - buffer_size, + max(lats) + buffer_size, + min(lons) - buffer_size, + max(lons) + buffer_size + ) - try: - if (lats.size == 1) & (lons.size == 1): - out = [lats - buf, lats + buf, lons - buf, lons + buf] - elif (lats.size > 1) & (lons.size > 1): - out = [np.nanmin(lats), np.nanmax(lats), np.nanmin(lons), np.nanmax(lons)] - elif lats.size == 1: - out = [lats - buf, lats + buf, np.nanmin(lons), np.nanmax(lons)] - elif lons.size == 1: - out = [np.nanmin(lats), np.nanmax(lats), lons - buf, lons + buf] - except AttributeError: - if (isinstance(lats, tuple) or isinstance(lats, list)) and len(lats) == 2: - out = [min(lats) - buf, max(lats) + buf, min(lons) - buf, max(lons) + buf] - except Exception: - raise RuntimeError('Not a valid lat/lon shape or variable') - - return np.array(out) - - -def isOutside(extent1, extent2) -> bool: - """ - Determine whether any of extent1 lies outside extent2. - extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon]. + +def isOutside(extent1: BB.SNWE, extent2: BB.SNWE) -> bool: + """Determine whether any of extent1 lies outside extent2. + + extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon] (SNWE). Equal extents are considered "inside". """ t1 = extent1[0] < extent2[0] t2 = extent1[1] > extent2[1] t3 = extent1[2] < extent2[2] t4 = extent1[3] > extent2[3] - return np.any([t1, t2, t3, t4]) + return any((t1, t2, t3, t4)) -def isInside(extent1, extent2) -> bool: - """ - Determine whether all of extent1 lies inside extent2. - extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon]. +def isInside(extent1: BB.SNWE, extent2: BB.SNWE) -> bool: + """Determine whether all of extent1 lies inside extent2. + + extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon] (SNWE). Equal extents are considered "inside". """ t1 = extent1[0] <= extent2[0] t2 = extent1[1] >= extent2[1] t3 = extent1[2] <= extent2[2] t4 = extent1[3] >= extent2[3] - return np.all([t1, t2, t3, t4]) + return all((t1, t2, t3, t4)) # below are for downloadGNSSDelays -def date_type(arg): +def date_type(val: Union[int, str]) -> dt.date: """Parse a date from a string in pseudo-ISO 8601 format.""" - year_formats = ( - '%Y-%m-%d', - '%Y%m%d', - '%d', - '%j', - ) - - for yf in year_formats: - try: - return date(*strptime(arg, yf)[0:3]) - except ValueError: - pass - - raise ArgumentTypeError(f'Unable to coerce {arg} to a date. Try %Y-%m-%d') + try: + return coerce_into_date(val) + except ValueError as exc: + raise argparse.ArgumentTypeError(str(exc)) class MappingType: - """ - A type that maps arguments to constants. + """A type that maps arguments to constants. # Example ``` @@ -393,17 +340,18 @@ class MappingType: """ UNSET = object() + _default: Union[object, Any] - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs: dict[str, Any]) -> None: self.mapping = kwargs self._default = self.UNSET - def default(self, default): + def default(self, default: Any) -> Self: # noqa: ANN401 """Set a default value if no mapping is found.""" self._default = default return self - def __call__(self, arg): + def __call__(self, arg: str) -> Any: # noqa: ANN401 if arg in self.mapping: return self.mapping[arg] @@ -413,9 +361,8 @@ def __call__(self, arg): return self._default -class IntegerType: - """ - A type that converts arguments to integers. +class IntegerOnRangeType: + """A type that converts arguments to integers and enforces that they are on a certain range. # Example ``` @@ -426,24 +373,23 @@ class IntegerType: ``` """ - def __init__(self, lo=None, hi=None) -> None: + def __init__(self, lo: Optional[int]=None, hi: Optional[int]=None) -> None: self.lo = lo self.hi = hi - def __call__(self, arg): + def __call__(self, arg: Any) -> int: # noqa: ANN401 integer = int(arg) if self.lo is not None and integer < self.lo: - raise ArgumentTypeError(f'Must be greater than {self.lo}') + raise argparse.ArgumentTypeError(f'Must be greater than {self.lo}') if self.hi is not None and integer > self.hi: - raise ArgumentTypeError(f'Must be less than {self.hi}') + raise argparse.ArgumentTypeError(f'Must be less than {self.hi}') return integer -class IntegerMappingType(MappingType, IntegerType): - """ - An integer type that converts non-integer types through a mapping. +class IntegerMappingType(MappingType, IntegerOnRangeType): + """An integer type that converts non-integer types through a mapping. # Example ``` @@ -454,33 +400,33 @@ class IntegerMappingType(MappingType, IntegerType): ``` """ - def __init__(self, lo=None, hi=None, mapping={}, **kwargs) -> None: - IntegerType.__init__(self, lo, hi) + def __init__(self, lo: Optional[int]=None, hi: Optional[int]=None, mapping: Optional[dict[str, Any]]={}, **kwargs: dict[str, Any]) -> None: + IntegerOnRangeType.__init__(self, lo, hi) kwargs.update(mapping) MappingType.__init__(self, **kwargs) - def __call__(self, arg): + def __call__(self, arg: Any) -> Union[int, Any]: # noqa: ANN401 try: - return IntegerType.__call__(self, arg) + return IntegerOnRangeType.__call__(self, arg) except ValueError: return MappingType.__call__(self, arg) -class DateListAction(Action): +class DateListAction(argparse.Action): """An Action that parses and stores a list of dates.""" def __init__( self, - option_strings, - dest, - nargs=None, - const=None, - default=None, - type=None, - choices=None, - required=False, - help=None, - metavar=None, + option_strings, # noqa: ANN001 -- see argparse.Action.__init__ + dest, # noqa: ANN001 + nargs=None, # noqa: ANN001 + const=None, # noqa: ANN001 + default=None, # noqa: ANN001 + type=None, # noqa: ANN001 + choices=None, # noqa: ANN001 + required=False, # noqa: ANN001 + help=None, # noqa: ANN001 + metavar=None, # noqa: ANN001 ) -> None: if type is not date_type: raise ValueError('type must be `date_type`!') @@ -498,42 +444,42 @@ def __init__( metavar=metavar, ) - def __call__(self, parser, namespace, values, option_string=None): + def __call__(self, _, namespace, values, __=None): # noqa: ANN001, ANN204 -- see argparse.Action.__call__ if len(values) > 3 or not values: - raise ArgumentError(self, 'Only 1, 2 dates, or 2 dates and interval may be supplied') + raise argparse.ArgumentError(self, 'Only 1, 2 dates, or 2 dates and interval may be supplied') if len(values) == 2: start, end = values - values = [start + timedelta(days=k) for k in range(0, (end - start).days + 1, 1)] + values = [start + dt.timedelta(days=k) for k in range(0, (end - start).days + 1, 1)] elif len(values) == 3: start, end, stepsize = values if not isinstance(stepsize.day, int): - raise ArgumentError(self, 'The stepsize should be in integer days') + raise argparse.ArgumentError(self, 'The stepsize should be in integer days') - new_year = date(year=stepsize.year, month=1, day=1) + new_year = dt.date(year=stepsize.year, month=1, day=1) stepsize = (stepsize - new_year).days + 1 - values = [start + timedelta(days=k) for k in range(0, (end - start).days + 1, stepsize)] + values = [start + dt.timedelta(days=k) for k in range(0, (end - start).days + 1, stepsize)] setattr(namespace, self.dest, values) -class BBoxAction(Action): +class BBoxAction(argparse.Action): """An Action that parses and stores a valid bounding box.""" def __init__( self, - option_strings, - dest, - nargs=None, - const=None, - default=None, - type=None, - choices=None, - required=False, - help=None, - metavar=None, + option_strings, # noqa: ANN001 -- see argparse.Action.__init__ + dest, # noqa: ANN001 + nargs=None, # noqa: ANN001 + const=None, # noqa: ANN001 + default=None, # noqa: ANN001 + type=None, # noqa: ANN001 + choices=None, # noqa: ANN001 + required=False, # noqa: ANN001 + help=None, # noqa: ANN001 + metavar=None, # noqa: ANN001 ) -> None: if nargs != 4: raise ValueError('nargs must be 4!') @@ -551,19 +497,19 @@ def __init__( metavar=metavar, ) - def __call__(self, parser, namespace, values, option_string=None): + def __call__(self, _, namespace, values, __=None): # noqa: ANN001, ANN204 -- see argparse.Action.__call__ S, N, W, E = values if N <= S or E <= W: - raise ArgumentError(self, 'Bounding box has no size; make sure you use "S N W E"') + raise argparse.ArgumentError(self, 'Bounding box has no size; make sure you use "S N W E"') for sn in (S, N): if sn < -90 or sn > 90: - raise ArgumentError(self, 'Lats are out of S/N bounds (-90 to 90).') + raise argparse.ArgumentError(self, 'Lats are out of S/N bounds (-90 to 90).') for we in (W, E): if we < -180 or we > 180: - raise ArgumentError( + raise argparse.ArgumentError( self, 'Lons are out of W/E bounds (-180 to 180); Lons in the format of (0 to 360) are not supported.', ) From 8147520e3bd2c37f5555412b139808db1aaa8e71 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:53:29 -0500 Subject: [PATCH 34/76] calcDelays: fix path to out file being lost --- tools/RAiDER/cli/raider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 0a704eaff..672432c46 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -375,7 +375,7 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: else: out_filename = Path(out_filename) if aoi.type() == 'station_file': - out_filename = Path(out_filename.stem + '.csv') + out_filename = out_filename.with_suffix('.csv') if aoi.type() in ('station_file', 'radar_rasters', 'geocoded_file'): writeDelays(aoi, wet_delay, hydro_delay, str(out_filename), f, outformat=run_config.runtime_group.raster_format) From 0fd664be285fe69fab4a902023201a1d56c70803 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:20:22 -0500 Subject: [PATCH 35/76] Call calcDelays directly for debugging These tests are easier to debug when they import calcDelays and call it directly, instead of running calcDelays through raider.py. --- test/test_intersect.py | 9 +++------ test/test_slant.py | 4 +--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/test/test_intersect.py b/test/test_intersect.py index 0a2ffed64..1cc452ee0 100644 --- a/test/test_intersect.py +++ b/test/test_intersect.py @@ -1,3 +1,4 @@ +from RAiDER.cli.raider import calcDelays from RAiDER.utilFcns import write_yaml import pytest import os @@ -43,9 +44,7 @@ def test_cube_intersect(wm): cfg = write_yaml(grp, 'temp.yaml') ## run raider and intersect - cmd = f'raider.py {cfg}' - proc = subprocess.run(cmd.split(), stdout=subprocess.PIPE, universal_newlines=True) - assert proc.returncode == 0, 'RAiDER Failed.' + calcDelays([str(cfg)]) ## hard code what it should be and check it matches gold = {'ERA5': 2.2787, 'GMAO': np.nan, 'HRRR': np.nan} @@ -89,9 +88,7 @@ def test_gnss_intersect(wm): cfg = write_yaml(grp, 'temp.yaml') ## run raider and intersect - cmd = f'raider.py {cfg}' - proc = subprocess.run(cmd.split(), stdout=subprocess.PIPE, universal_newlines=True) - assert proc.returncode == 0, 'RAiDER Failed.' + calcDelays([str(cfg)]) gold = {'ERA5': 2.34514, 'GMAO': np.nan, 'HRRR': np.nan} df = pd.read_csv(os.path.join(SCENARIO_DIR, f'{wm}_Delay_{date}T{time.replace(":", "")}_ztd.csv')) diff --git a/test/test_slant.py b/test/test_slant.py index 60eceb4a4..e13f0b530 100644 --- a/test/test_slant.py +++ b/test/test_slant.py @@ -44,9 +44,7 @@ def test_slant_proj(weather_model_name): cfg = write_yaml(grp, 'temp.yaml') ## run raider and intersect - cmd = f'raider.py {cfg}' - proc = subprocess.run(cmd.split(), stdout=subprocess.PIPE, universal_newlines=True) - assert proc.returncode == 0, 'RAiDER Failed.' + calcDelays([str(cfg)]) gold = {'ERA5': [33.4, -117.8, 0, 2.333865144]} lat, lon, hgt, val = gold[weather_model_name] From 3cdc4fe92d813e2e983c8ef1f4f38dc5d923d23e Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:22:54 -0500 Subject: [PATCH 36/76] Formatting --- tools/RAiDER/cli/raider.py | 7 ++++--- tools/RAiDER/cli/validators.py | 1 - tools/RAiDER/delay.py | 16 ++++++++-------- tools/RAiDER/getStationDelays.py | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 672432c46..128a7f366 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -345,12 +345,13 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: # Different options depending on the inputs if los.is_Projected(): out_filename = w.replace('_ztd', '_std') - f = f.replace('_ztd', '_std') + hydro_filename = f.replace('_ztd', '_std') elif los.ray_trace(): out_filename = w.replace('_std', '_ray') - f = f.replace('_std', '_ray') + hydro_filename = f.replace('_std', '_ray') else: out_filename = w + hydro_filename = f # A dataset was returned by the above # Dataset returned: Cube e.g. GUNW workflow @@ -378,7 +379,7 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: out_filename = out_filename.with_suffix('.csv') if aoi.type() in ('station_file', 'radar_rasters', 'geocoded_file'): - writeDelays(aoi, wet_delay, hydro_delay, str(out_filename), f, outformat=run_config.runtime_group.raster_format) + writeDelays(aoi, wet_delay, hydro_delay, out_filename, Path(hydro_filename), outformat=run_config.runtime_group.raster_format) wet_paths.append(out_filename) diff --git a/tools/RAiDER/cli/validators.py b/tools/RAiDER/cli/validators.py index b1ad4544c..3b080ce57 100755 --- a/tools/RAiDER/cli/validators.py +++ b/tools/RAiDER/cli/validators.py @@ -243,7 +243,6 @@ def parse_dates(date_group: DateGroupUnparsed) -> DateGroup: ) - def coerce_into_date(val: Union[int, str]) -> dt.date: """Parse a date from a string in pseudo-ISO 8601 format.""" year_formats = ( diff --git a/tools/RAiDER/delay.py b/tools/RAiDER/delay.py index 2bbe69804..3f0094d0a 100755 --- a/tools/RAiDER/delay.py +++ b/tools/RAiDER/delay.py @@ -38,8 +38,9 @@ def tropo_delay( out_proj: Union[int, str] = 4326, zref: Union[int, float] = _ZREF, ): - """ - Calculate integrated delays on query points. Options are: + """Calculate integrated delays on query points. + + Options are: 1. Zenith delays (ZTD) 2. Zenith delays projected to the line-of-sight (STD-projected) 3. Slant delays integrated along the raypath (STD-raytracing) @@ -53,7 +54,6 @@ def tropo_delay( out_proj: int,str - (optional) EPSG code for output projection zref: int,float - (optional) maximum height to integrate up to during raytracing - Returns: xarray Dataset *or* ndarrays: - wet and hydrostatic delays at the grid nodes / query points. """ @@ -75,12 +75,12 @@ def tropo_delay( toa = wm_levels.max() - 1 if height_levels is None: - if aoi.type() == 'Geocube': + if isinstance(aoi, Geocube): height_levels = aoi.readZ() else: height_levels = wm_levels - if not zref: + if zref == 0: zref = toa if zref > toa: @@ -92,7 +92,7 @@ def tropo_delay( # TODO: expose this as library function ds = _get_delays_on_cube(dt, weather_model_file, wm_proj, aoi, height_levels, los, crs, zref) - if (aoi.type() == 'bounding_box') or (aoi.type() == 'Geocube'): + if isinstance(aoi, (BoundingBox, Geocube)): return ds, None else: @@ -100,7 +100,7 @@ def tropo_delay( try: out_proj = CRS.from_epsg(out_proj) except pyproj.exceptions.CRSError: - out_proj = out_proj + pass pnt_proj = CRS.from_epsg(4326) lats, lons = aoi.readLL() @@ -110,7 +110,7 @@ def tropo_delay( try: ifWet, ifHydro = getInterpolators(ds, 'ztd') except RuntimeError: - logger.exception('Failed to get weather model %s interpolators.', weather_model_file) + raise RuntimeError(f'Failed to get weather model {weather_model_file} interpolators.') wetDelay = ifWet(pnts) hydroDelay = ifHydro(pnts) diff --git a/tools/RAiDER/getStationDelays.py b/tools/RAiDER/getStationDelays.py index 3e78987e2..c7b4ec040 100644 --- a/tools/RAiDER/getStationDelays.py +++ b/tools/RAiDER/getStationDelays.py @@ -230,7 +230,7 @@ def get_station_data(inFile, dateList, gps_repo=None, numCPUs=8, outDir=None, re # confirm file exists (i.e. valid delays exists for specified time/region). outputfiles = [i for i in outputfiles if os.path.exists(i)] # Consolidate all CSV files into one object - if outputfiles == []: + if len(outputfiles) == 0: raise Exception('No valid delays found for specified time/region.') name = os.path.join(outDir, f'{gps_repo}combinedGPS_ztd.csv') statsFile = pd.concat([pd.read_csv(i) for i in outputfiles]) From f3b1dbfcfcaadc62d01a2d4e9a46fb53c8838891 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:23:42 -0500 Subject: [PATCH 37/76] Add type annotations, use Path --- tools/RAiDER/delay.py | 15 ++++++++------- tools/RAiDER/delayFcns.py | 13 +++++++------ tools/RAiDER/utilFcns.py | 20 ++++++++++++++------ 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/tools/RAiDER/delay.py b/tools/RAiDER/delay.py index 3f0094d0a..af31d4c1e 100755 --- a/tools/RAiDER/delay.py +++ b/tools/RAiDER/delay.py @@ -15,7 +15,7 @@ import os from datetime import datetime, timezone -from typing import List, Union +from typing import Optional, Union import numpy as np import pyproj @@ -24,19 +24,20 @@ from RAiDER.constants import _ZREF from RAiDER.delayFcns import getInterpolators +from RAiDER.llreader import AOI, BoundingBox, Geocube from RAiDER.logger import logger -from RAiDER.losreader import build_ray +from RAiDER.losreader import LOS, build_ray ############################################################################### def tropo_delay( - dt, + dt: datetime, weather_model_file: str, - aoi, - los, - height_levels: List[float] = None, + aoi: AOI, + los: LOS, + height_levels: Optional[list[float]] = None, out_proj: Union[int, str] = 4326, - zref: Union[int, float] = _ZREF, + zref: np.float64 = _ZREF, ): """Calculate integrated delays on query points. diff --git a/tools/RAiDER/delayFcns.py b/tools/RAiDER/delayFcns.py index e6a2223d9..e55823fae 100755 --- a/tools/RAiDER/delayFcns.py +++ b/tools/RAiDER/delayFcns.py @@ -10,14 +10,18 @@ except ImportError: mp = None +from pathlib import Path +from typing import Union + import numpy as np -import xarray +import xarray as xr from scipy.interpolate import RegularGridInterpolator as Interpolator from RAiDER.logger import logger -def getInterpolators(wm_file, kind='pointwise', shared=False): +# TODO(garlic-os): type annotate the choices for kind +def getInterpolators(wm_file: Union[xr.Dataset, Path, str], kind: str='pointwise', shared: bool=False) -> tuple[Interpolator, Interpolator]: """ Read 3D gridded data from a processed weather model file and wrap it with the scipy RegularGridInterpolator. @@ -25,10 +29,7 @@ def getInterpolators(wm_file, kind='pointwise', shared=False): The interpolator grid is (y, x, z) """ # Get the weather model data - try: - ds = xarray.load_dataset(wm_file) - except ValueError: - ds = wm_file + ds = wm_file if isinstance(wm_file, xr.Dataset) else xr.load_dataset(wm_file) xs_wm = np.array(ds.variables['x'][:]) ys_wm = np.array(ds.variables['y'][:]) diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 90aac6504..e53952d23 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -4,7 +4,7 @@ import re from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Any, Union +from typing import Any, Optional, Union import numpy as np import rasterio @@ -407,7 +407,15 @@ def round_time(dt, roundTo=60): return dt + timedelta(0, rounding - seconds, -dt.microsecond) -def writeDelays(aoi, wetDelay, hydroDelay, wetFilename, hydroFilename=None, outformat=None, ndv=0.0) -> None: +def writeDelays( + aoi, #: AOI, + wetDelay, + hydroDelay, + wet_path: Path, + hydro_filename: Optional[str]=None, + outformat: str=None, + ndv: float=0.0 +) -> None: """Write the delay numpy arrays to files in the format specified.""" if pd is None: raise ImportError('pandas is required to write GNSS delays to a file') @@ -423,14 +431,14 @@ def writeDelays(aoi, wetDelay, hydroDelay, wetFilename, hydroFilename=None, outf df['wetDelay'] = wetDelay df['hydroDelay'] = hydroDelay df['totalDelay'] = wetDelay + hydroDelay - df.to_csv(wetFilename, index=False) - logger.info('Wrote delays to: %s', wetFilename) + df.to_csv(str(wet_path), index=False) + logger.info('Wrote delays to: %s', wet_path.absolute()) else: proj = aoi.projection() gt = aoi.geotransform() - writeArrayToRaster(wetDelay, wetFilename, noDataValue=ndv, fmt=outformat, proj=proj, gt=gt) - writeArrayToRaster(hydroDelay, hydroFilename, noDataValue=ndv, fmt=outformat, proj=proj, gt=gt) + writeArrayToRaster(wetDelay, str(wet_path), noDataValue=ndv, fmt=outformat, proj=proj, gt=gt) + writeArrayToRaster(hydroDelay, hydro_filename, noDataValue=ndv, fmt=outformat, proj=proj, gt=gt) def getTimeFromFile(filename): From c9f932491fdbb76442d91b8d0d2fa6377d761f8f Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:39:39 -0500 Subject: [PATCH 38/76] Fix merge conflict --- tools/RAiDER/cli/raider.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 128a7f366..0edabf246 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -90,6 +90,9 @@ def read_run_config_file(path: Path) -> RunConfig: print(exc) raise ValueError(f'Something is wrong with the yaml file {path}') + # Drop any values not specified + yaml_data = drop_nans(yaml_data) + # Ensure that all the groups exist, even if they are not specified by the user for key in ('date_group', 'time_group', 'aoi_group', 'height_group', 'los_group', 'runtime_group'): if key not in yaml_data or yaml_data[key] is None: From 44abcbdf2258c0188ccd4f1f35f27d4d47c2bc1c Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 31 Jul 2024 22:41:23 -0500 Subject: [PATCH 39/76] tropo_delay: default zref to toa, not _ZREF This is the behavior in a9bf37c. --- tools/RAiDER/cli/args.py | 6 +++--- tools/RAiDER/delay.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/RAiDER/cli/args.py b/tools/RAiDER/cli/args.py index c6d7959a8..088e2d4f5 100644 --- a/tools/RAiDER/cli/args.py +++ b/tools/RAiDER/cli/args.py @@ -8,7 +8,7 @@ import numpy as np -from RAiDER.constants import _CUBE_SPACING_IN_M, _ZREF +from RAiDER.constants import _CUBE_SPACING_IN_M from RAiDER.llreader import AOI from RAiDER.losreader import LOS from RAiDER.models.weatherModel import WeatherModel @@ -141,7 +141,7 @@ class LOSGroupUnparsed: los_convention: LOSConvention = 'isce' los_cube: Optional[str] = None orbit_file: Optional[str] = None - zref: np.float64 = _ZREF + zref: Optional[np.float64] = None @dataclasses.dataclass class LOSGroup: @@ -151,7 +151,7 @@ class LOSGroup: los_convention: LOSConvention = 'isce' los_cube: Optional[str] = None orbit_file: Optional[str] = None - zref: np.float64 = _ZREF + zref: Optional[np.float64] = None class RuntimeGroup: raster_format: str diff --git a/tools/RAiDER/delay.py b/tools/RAiDER/delay.py index af31d4c1e..bf51e1998 100755 --- a/tools/RAiDER/delay.py +++ b/tools/RAiDER/delay.py @@ -37,7 +37,7 @@ def tropo_delay( los: LOS, height_levels: Optional[list[float]] = None, out_proj: Union[int, str] = 4326, - zref: np.float64 = _ZREF, + zref: Optional[np.float64] = None, ): """Calculate integrated delays on query points. @@ -81,7 +81,7 @@ def tropo_delay( else: height_levels = wm_levels - if zref == 0: + if zref is None: zref = toa if zref > toa: From 2ac79c863e027363142718a2badba1028189e08b Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 31 Jul 2024 23:39:11 -0500 Subject: [PATCH 40/76] Fix regression: cube_spacing_in_m missing from AOI Fixes a regression from 5fe4e0f50364c9ebdb98d071acd47b7a9c6a1240 line 128: I mistook params['aoi'] for params['aoi_group'] and just deleted this line. It turns out the rest of the program really does depend on cube_spacing_in_m to be situated on the AOI object. This change reimplemens that in a way where cube_spacing_in_m is documented to be a property AOI. --- tools/RAiDER/cli/raider.py | 3 ++- tools/RAiDER/cli/validators.py | 19 ++++++++++--------- tools/RAiDER/llreader.py | 25 +++++++++++++------------ 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 0edabf246..f980c47aa 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -116,7 +116,8 @@ def read_run_config_file(path: Path) -> RunConfig: aoi_group = AOIGroup( aoi=get_query_region( aoi_group_unparsed, - height_group_unparsed + height_group_unparsed, + cube_spacing_in_m=runtime_group.cube_spacing_in_m, ) ) diff --git a/tools/RAiDER/cli/validators.py b/tools/RAiDER/cli/validators.py index 3b080ce57..220e5576c 100755 --- a/tools/RAiDER/cli/validators.py +++ b/tools/RAiDER/cli/validators.py @@ -129,7 +129,7 @@ def get_heights(height_group: HeightGroupUnparsed, aoi_group: AOIGroupUnparsed, return result -def get_query_region(aoi_group: AOIGroupUnparsed, height_group: HeightGroupUnparsed) -> AOI: +def get_query_region(aoi_group: AOIGroupUnparsed, height_group: HeightGroupUnparsed, cube_spacing_in_m: float) -> AOI: """Parse the query region from inputs. This function determines the query region from the input parameters. It will return an AOI object that can be used @@ -139,38 +139,39 @@ def get_query_region(aoi_group: AOIGroupUnparsed, height_group: HeightGroupUnpar # Get bounds from the inputs # make sure this is first if height_group.use_dem_latlon: - query = GeocodedFile(height_group.dem, is_dem=True) + query = GeocodedFile(Path(height_group.dem), is_dem=True, cube_spacing_in_m=cube_spacing_in_m) elif aoi_group.lat_file is not None or aoi_group.lon_file is not None: if aoi_group.lat_file is None or aoi_group.lon_file is None: raise ValueError('A lon_file must be specified if a lat_file is specified') query = RasterRDR( aoi_group.lat_file, aoi_group.lon_file, - height_group.height_file_rdr, height_group.dem + height_group.height_file_rdr, height_group.dem, + cube_spacing_in_m=cube_spacing_in_m ) elif aoi_group.station_file is not None: - query = StationFile(aoi_group.station_file) + query = StationFile(aoi_group.station_file, cube_spacing_in_m=cube_spacing_in_m) elif aoi_group.bounding_box is not None: bbox = parse_bbox(aoi_group.bounding_box) if np.min(bbox[0]) < -90 or np.max(bbox[1]) > 90: raise ValueError('Lats are out of N/S bounds; are your lat/lon coordinates switched? Should be SNWE') - query = BoundingBox(bbox) + query = BoundingBox(bbox, cube_spacing_in_m=cube_spacing_in_m) elif aoi_group.geocoded_file is not None: - filename = Path(aoi_group.geocoded_file).name.upper() + geocoded_file_path = Path(aoi_group.geocoded_file) + filename = geocoded_file_path.name.upper() if filename.startswith('SRTM') or filename.startswith('GLO'): logger.debug('Using user DEM: %s', filename) is_dem = True else: is_dem = False - - query = GeocodedFile(aoi_group.geocoded_file, is_dem=is_dem) + query = GeocodedFile(geocoded_file_path, is_dem=is_dem, cube_spacing_in_m=cube_spacing_in_m) # untested elif aoi_group.geo_cube is not None: - query = Geocube(aoi_group.geo_cube) + query = Geocube(aoi_group.geo_cube, cube_spacing_in_m) else: # TODO: Need to incorporate the cube diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index ce06868bc..ce7bdc4dc 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -9,6 +9,7 @@ from pathlib import Path from typing import Union +from RAiDER.constants import _CUBE_SPACING_IN_M from RAiDER.types import BB, RIOProfile import numpy as np import pyproj @@ -36,12 +37,12 @@ class AOI: _type - Type of AOI """ - def __init__(self) -> None: + def __init__(self, cube_spacing_in_m: float = _CUBE_SPACING_IN_M) -> None: self._output_directory = os.getcwd() self._bounding_box = None self._proj = CRS.from_epsg(4326) self._geotransform = None - self._cube_spacing_m = None + self._cube_spacing_m = cube_spacing_in_m def type(self): return self._type @@ -190,8 +191,8 @@ def set_output_xygrid(self, dst_crs: Union[int, str]=4326) -> None: class StationFile(AOI): """Use a .csv file containing at least Lat, Lon, and optionally Hgt_m columns.""" - def __init__(self, station_file, demFile=None) -> None: - super().__init__() + def __init__(self, station_file, demFile=None, cube_spacing_in_m: float = _CUBE_SPACING_IN_M) -> None: + super().__init__(cube_spacing_in_m) self._filename = station_file self._demfile = demFile self._bounding_box = bounds_from_csv(station_file) @@ -240,8 +241,8 @@ def readZ(self): class RasterRDR(AOI): """Use a 2-band raster file containing lat/lon coordinates.""" - def __init__(self, lat_file, lon_file=None, hgt_file=None, dem_file=None, convention='isce') -> None: - super().__init__() + def __init__(self, lat_file, lon_file=None, hgt_file=None, dem_file=None, convention='isce', cube_spacing_in_m: float = _CUBE_SPACING_IN_M) -> None: + super().__init__(cube_spacing_in_m) self._type = 'radar_rasters' self._latfile = lat_file self._lonfile = lon_file @@ -302,8 +303,8 @@ def readZ(self): class BoundingBox(AOI): """Parse a bounding box AOI.""" - def __init__(self, bbox) -> None: - AOI.__init__(self) + def __init__(self, bbox, cube_spacing_in_m: float = _CUBE_SPACING_IN_M) -> None: + super().__init__(cube_spacing_in_m) self._bounding_box = bbox self._type = 'bounding_box' @@ -315,8 +316,8 @@ class GeocodedFile(AOI): _bounding_box: BB.SNWE _is_dem: bool - def __init__(self, path: Path, is_dem=False) -> None: - super().__init__() + def __init__(self, path: Path, is_dem=False, cube_spacing_in_m: float = _CUBE_SPACING_IN_M) -> None: + super().__init__(cube_spacing_in_m) from RAiDER.utilFcns import rio_extents, rio_profile @@ -358,8 +359,8 @@ def readZ(self): class Geocube(AOI): """Pull lat/lon/height from a georeferenced data cube.""" - def __init__(self, path_cube) -> None: - super().__init__() + def __init__(self, path_cube, cube_spacing_in_m: float = _CUBE_SPACING_IN_M) -> None: + super().__init__(cube_spacing_in_m) self.path = path_cube self._type = 'Geocube' self._bounding_box = self.get_extent() From 7f5806585bc778600c5428d4762ca23b327d9e8d Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:36:15 -0500 Subject: [PATCH 41/76] Do not cast float argument to string --- test/test_synthetic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_synthetic.py b/test/test_synthetic.py index 17819e12a..4a39fc8d0 100644 --- a/test/test_synthetic.py +++ b/test/test_synthetic.py @@ -175,7 +175,7 @@ def make_config_dict(self): 'weather_model': self.wmName, 'runtime_group': { 'output_directory': self.wd, - 'cube_spacing_in_m': str(self._cube_spacing_m), + 'cube_spacing_in_m': self._cube_spacing_m, }, } return dct From eadf31b46679dce92995bd45d6484a792c736cb4 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:36:31 -0500 Subject: [PATCH 42/76] Call calcDelays directly for debugging --- test/test_synthetic.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/test/test_synthetic.py b/test/test_synthetic.py index 4a39fc8d0..537710f56 100644 --- a/test/test_synthetic.py +++ b/test/test_synthetic.py @@ -1,3 +1,4 @@ +from RAiDER.cli.raider import calcDelays import pytest import os.path as op @@ -196,9 +197,9 @@ def test_dl_real(region, mod='ERA5'): dct_cfg['download_only'] = True cfg = write_yaml(dct_cfg, 'temp.yaml') + ## run raider to download the real weather model cmd = f'raider.py {cfg}' - proc = subprocess.run(cmd.split(), stdout=subprocess.PIPE, universal_newlines=True) assert proc.returncode == 0, 'RAiDER did not complete successfully' @@ -233,9 +234,7 @@ def test_hydrostatic_eq(region, mod='ERA-5'): cfg = write_yaml(dct_cfg, 'temp.yaml') ## run raider with the synthetic model - cmd = f'raider.py {cfg}' - proc = subprocess.run(cmd.split(), stdout=subprocess.PIPE, universal_newlines=True) - assert proc.returncode == 0, 'RAiDER did not complete successfully' + calcDelays([str(cfg)]) # get the just created synthetic delays wm_name = SAobj.wmName.replace('-', '') # incase of ERA-5 @@ -302,9 +301,7 @@ def test_wet_eq_linear(region, mod='ERA-5'): cfg = write_yaml(dct_cfg, 'temp.yaml') ## run raider with the synthetic model - cmd = f'raider.py {cfg}' - proc = subprocess.run(cmd.split(), stdout=subprocess.PIPE, universal_newlines=True) - assert proc.returncode == 0, 'RAiDER did not complete successfully' + calcDelays([str(cfg)]) # get the just created synthetic delays wm_name = SAobj.wmName.replace('-', '') # incase of ERA-5 @@ -369,9 +366,7 @@ def test_wet_eq_nonlinear(region, mod='ERA-5'): cfg = write_yaml(dct_cfg, 'temp.yaml') ## run raider with the synthetic model - cmd = f'raider.py {cfg}' - proc = subprocess.run(cmd.split(), stdout=subprocess.PIPE, universal_newlines=True) - assert proc.returncode == 0, 'RAiDER did not complete successfully' + calcDelays([str(cfg)]) # get the just created synthetic delays wm_name = SAobj.wmName.replace('-', '') # incase of ERA-5 From c849a91b2f432cd6eb65ea314b4fd9f1156216f1 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:38:16 -0500 Subject: [PATCH 43/76] Default cube_spacing_in_m to None Not the behavior I expected, but the behavior the project currently has --- tools/RAiDER/llreader.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index ce7bdc4dc..280d2a8e3 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -7,7 +7,7 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import os from pathlib import Path -from typing import Union +from typing import Any, Optional, Union from RAiDER.constants import _CUBE_SPACING_IN_M from RAiDER.types import BB, RIOProfile @@ -37,7 +37,7 @@ class AOI: _type - Type of AOI """ - def __init__(self, cube_spacing_in_m: float = _CUBE_SPACING_IN_M) -> None: + def __init__(self, cube_spacing_in_m: Optional[float]=None) -> None: self._output_directory = os.getcwd() self._bounding_box = None self._proj = CRS.from_epsg(4326) @@ -191,7 +191,7 @@ def set_output_xygrid(self, dst_crs: Union[int, str]=4326) -> None: class StationFile(AOI): """Use a .csv file containing at least Lat, Lon, and optionally Hgt_m columns.""" - def __init__(self, station_file, demFile=None, cube_spacing_in_m: float = _CUBE_SPACING_IN_M) -> None: + def __init__(self, station_file, demFile=None, cube_spacing_in_m: Optional[float]=None) -> None: super().__init__(cube_spacing_in_m) self._filename = station_file self._demfile = demFile @@ -241,7 +241,7 @@ def readZ(self): class RasterRDR(AOI): """Use a 2-band raster file containing lat/lon coordinates.""" - def __init__(self, lat_file, lon_file=None, hgt_file=None, dem_file=None, convention='isce', cube_spacing_in_m: float = _CUBE_SPACING_IN_M) -> None: + def __init__(self, lat_file, lon_file=None, hgt_file=None, dem_file=None, convention='isce', cube_spacing_in_m: Optional[float]=None) -> None: super().__init__(cube_spacing_in_m) self._type = 'radar_rasters' self._latfile = lat_file @@ -303,7 +303,7 @@ def readZ(self): class BoundingBox(AOI): """Parse a bounding box AOI.""" - def __init__(self, bbox, cube_spacing_in_m: float = _CUBE_SPACING_IN_M) -> None: + def __init__(self, bbox, cube_spacing_in_m: Optional[float]=None) -> None: super().__init__(cube_spacing_in_m) self._bounding_box = bbox self._type = 'bounding_box' @@ -316,7 +316,7 @@ class GeocodedFile(AOI): _bounding_box: BB.SNWE _is_dem: bool - def __init__(self, path: Path, is_dem=False, cube_spacing_in_m: float = _CUBE_SPACING_IN_M) -> None: + def __init__(self, path: Path, is_dem=False, cube_spacing_in_m: Optional[float]=None) -> None: super().__init__(cube_spacing_in_m) from RAiDER.utilFcns import rio_extents, rio_profile @@ -359,7 +359,7 @@ def readZ(self): class Geocube(AOI): """Pull lat/lon/height from a georeferenced data cube.""" - def __init__(self, path_cube, cube_spacing_in_m: float = _CUBE_SPACING_IN_M) -> None: + def __init__(self, path_cube, cube_spacing_in_m: Optional[float]=None) -> None: super().__init__(cube_spacing_in_m) self.path = path_cube self._type = 'Geocube' From c5495480863eaf1869d0d08bca8e6f23fd57913e Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:41:54 -0500 Subject: [PATCH 44/76] Move rasterio typings to separate file --- tools/RAiDER/llreader.py | 4 ++-- tools/RAiDER/types/RIO.py | 27 +++++++++++++++++++++++++++ tools/RAiDER/types/__init__.py | 14 +------------- tools/RAiDER/utilFcns.py | 6 +++--- 4 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 tools/RAiDER/types/RIO.py diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index 280d2a8e3..a89c01712 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -10,7 +10,7 @@ from typing import Any, Optional, Union from RAiDER.constants import _CUBE_SPACING_IN_M -from RAiDER.types import BB, RIOProfile +from RAiDER.types import BB, RIO import numpy as np import pyproj import xarray @@ -312,7 +312,7 @@ def __init__(self, bbox, cube_spacing_in_m: Optional[float]=None) -> None: class GeocodedFile(AOI): """Parse a Geocoded file for coordinates.""" - p: RIOProfile + p: RIO.Profile _bounding_box: BB.SNWE _is_dem: bool diff --git a/tools/RAiDER/types/RIO.py b/tools/RAiDER/types/RIO.py new file mode 100644 index 000000000..0340cc271 --- /dev/null +++ b/tools/RAiDER/types/RIO.py @@ -0,0 +1,27 @@ +"""Polyfills for several symbols used for types that rasterio doesn't export.""" + +from dataclasses import dataclass +from typing import TypedDict, Union + +import rasterio.crs +import rasterio.transform + + +GDAL = tuple[float, float, float, float, float, float] + +@dataclass +class Statistics: + max: float + mean: float + min: float + std: float + + +class Profile(TypedDict): + driver: str + width: int + height: int + count: int + crs: Union[str, dict, rasterio.crs.CRS] + transform: rasterio.transform.Affine + dtype: str diff --git a/tools/RAiDER/types/__init__.py b/tools/RAiDER/types/__init__.py index e8be5b6f6..281b0598a 100644 --- a/tools/RAiDER/types/__init__.py +++ b/tools/RAiDER/types/__init__.py @@ -2,10 +2,7 @@ import argparse from pathlib import Path -from typing import Literal, Optional, TypedDict, Union - -import rasterio.crs -import rasterio.transform +from typing import Literal, Optional LookDir = Literal['right', 'left'] @@ -25,12 +22,3 @@ class CalcDelaysArgsUnparsed(argparse.Namespace): class CalcDelaysArgs(CalcDelaysArgsUnparsed): file: Path - -class RIOProfile(TypedDict): - driver: str - width: int - height: int - count: int - crs: Union[str, dict, rasterio.crs.CRS] - transform: rasterio.transform.Affine - dtype: str diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index e53952d23..4510f3ba0 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -22,7 +22,7 @@ _g1 as G1, ) from RAiDER.logger import logger -from RAiDER.types import BB, RIOProfile +from RAiDER.types import BB, RIO # Optional imports @@ -121,7 +121,7 @@ def ecef2enu(xyz, lat, lon, height): return np.stack((e, n, u), axis=-1) -def rio_profile(path: Path) -> RIOProfile: +def rio_profile(path: Path) -> RIO.Profile: """Reads the profile of a rasterio file.""" path_vrt = Path(f'{path}.vrt') @@ -135,7 +135,7 @@ def rio_profile(path: Path) -> RIOProfile: return src.profile -def rio_extents(profile: RIOProfile) -> BB.SNWE: +def rio_extents(profile: RIO.Profile) -> BB.SNWE: """Get a bounding box in SNWE from a rasterio profile.""" gt = profile['transform'].to_gdal() xSize = profile['width'] From 74564c231bff66df54b9a0ddf5385e1976a9b8f0 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:36:56 -0500 Subject: [PATCH 45/76] Use Path --- ...RA-5_2019_11_17_T20_51_58_5S_2S_41W_37W.nc | Bin 0 -> 807746 bytes test/test_dem.py | 8 +++--- test/test_llreader.py | 6 ++-- test/test_util.py | 26 +++++++++--------- tools/RAiDER/cli/raider.py | 25 +++++++++-------- tools/RAiDER/dem.py | 20 ++++++-------- tools/RAiDER/llreader.py | 6 ++-- tools/RAiDER/utilFcns.py | 16 ++++++----- 8 files changed, 54 insertions(+), 53 deletions(-) create mode 100644 test/synthetic_test/weather_files_synth/ERA-5_2019_11_17_T20_51_58_5S_2S_41W_37W.nc diff --git a/test/synthetic_test/weather_files_synth/ERA-5_2019_11_17_T20_51_58_5S_2S_41W_37W.nc b/test/synthetic_test/weather_files_synth/ERA-5_2019_11_17_T20_51_58_5S_2S_41W_37W.nc new file mode 100644 index 0000000000000000000000000000000000000000..1771cb7ba5dbf39d416b0ebaa613a1ca2a3e906c GIT binary patch literal 807746 zcmeFZWtbLMw*K9?H5S~0yK8W}(cl`~-Q6Afaks`HxO)ih?gV#t_u%jEq3OvPX3m+J z|9h_U33BbL>Z#hx@3q#hUDYvHt5hX^+|+TS#*Q5l`S0`Be-#^qez{cO>y35U+hwj( zty2A{@k>RCn(52MXoRHRQ{MM2Uiz$45*Gsq|$|NJwb* z;7PQQ(2&?6(L+MiyVszOy#{pc-K&2{5^ls~1SwP{Z^3-ULJNk#D_TgLh{+w=4(K?b zYtN2fW(H>mXBH|@ute*E1zQ*VsbQf4t&0|HU9@;;(crCUA@TnrYu&zIN2oh|Lt3bC z>jFQuE?B%FJPHTdgra9O{)-twI-u)-9ueYleiio%Y`S#pm!W6x4jp@BK%=%9+V<*@ zfgf#$W@z8GNBe<2+D4cYG$kbBd_jRh_afXF5+zE~;Pt3cqC{~APxu#kjFvR$xWpOC zAv^f5d`OXaAw@n%3kivlAb2NQuD=5I=jaIc?)~cCieD%~LXxEodNm|kXh>v8zdeRl z2u_U_l9UjjO6%Gk2ej|dsfc>_?$@DfFMQNLBoT6gTtY%Jd{GqHU;afNgFixZM}+39 zqg1m(g9cxm8;p8TsY=yrRj%EjdY#&>8>m9f$`Kd2UM0%Kxph>nMx>@O{##8WEG!kF zNl0mEBmc#EvHn$%N5qW&sUoh|`+7SxZBSUWkQfm$tX-j2e{nwhrh=n1Q8hQ9dpOVAj8-{1bIY@ zvNume$Ra@!`Y?p-j~0?Jq(Bhl^($AYT)%Q{Rc>9gdV_``#Rz#2rors(-*G@lND6)g zbE^ZzrB{E7QAq6I{fIDS_>u#0_#ONgd31htO%PDQf4zD~XcqZ<&)3Y1{GB>|1h>fF zB?iU#LKpda%b^H{k-s~i`TO6wi$+)&nS=L={rzuKE5EHu{c3f;*b()Md0+U4{0&s= zzN#Mc(pS~L)Mq9|R+X>E2)acBeWL^DQ@(1?nIv}EHN7_{*G+$&Ue`)Uh2hbYZ_Uq8P zSKF^mJ06UJ9vINEXP=J!+71}luVZMoAdddltULGX+M#vNwtf0^?bZ29)egcbNYbZY z?{1;wcMRr!T|Bh^FFh-`90*z+oZ6#zug+Zu4D8S`*q^_w7%~K#a_~xMkKh_3SQruo zJ@HqQ`cfEzjU<>Ak@Xkqe+O&+f4x73-=h~K++J06YJTzP z*OUv=1^-26-rz4cnHzEWZ`bpG6*}gd>o-nBr102pu1E42_s#W4KI6Z+9?56IH`gQi zgne^8l8@n=>ydnn-&~L6WBTTLBp>rP*CY8@zPTRB$NJ6nNItf2u1E5*e{(&OkK>!` zk$jxrT#w}A`sR8hpU8?78OKOIo^R$y^6`ChJ#xPPo9mJD!@s#6Ie+3e*CXeDy&N(z ze&pHr<982y_rP}#eD}b24}AB)cMp8`z;_RP_rP}#eD}crr3e1n`>fk9;!(fbSMmSR z-e&}jy=KkG9kKs68-^Mmnj$p{4S91eVlUx`uX_-|y`tcL%>Tc=qL7eI>$lwavcn!H zxF-{QAZ3TWdc@}9Upwp}VZCc`*VAm&UmETEx~=%XtsF2dBz2Q4jZq zt5CIYZJiLVo!i5;W@)(g3=Y@ww&9xfA)r171M1s1pl3+~%6q}Dd+Ytmcf+sGMJFmr zjEV9O4VSxCxGd$ubuBDhqsoUXEK0bxT??pAuYhXg4ye*?zxHqS>+%V|d~f_JTyCOL zEtsfG(I@JcCgCbyB3w;5E?N>$B2z$f`v;UDTR=(9`epdTue_&`dBv~fm;JiabD~n) zCaU&p^o1rzk#LPceykn=y?f=?#R36!yX#lzalcmH^J^#a5=0589oI&UnW*MvCaTJ& za8-U8P?ZG%wR|2>x&Z+ljvCOC_kK-0>DRSGekI{J@|s^W7|XG@_Udq@trD)*^}>~r z`E%k26unMBZPNsl2mZ0)?|kUj!&83s=X@`=-amp&jy=u?lzDbQbZhL1Q-f8y5%cx6KGs_0_-?AK2d1G?HdpoCQdD*gaJGUv`Gzgph%YhfaM z6f2%T&Z_i8ttqBdNX_kPFBnYTlO?-l$4t(c3<5#L@eqFlg*Du%oTKuP9iJ^j{0?uz0|JkQ~qDd zqNUjr4epD6Vn6l{`?3FQUvv}KgSbl1-eu&z==kXDQO1eb7j?78`Oo%6m$1M5&-O)o zu|FEQFFH5*KkkcWWFIwhUvvcfx50hU_w42VZ|{qa-u3^s-yZgxUx(Ru|4;VYE3fhE z=Q{@keqG<`S08L$x#wT+ zw{P9=*HmO~gFbYXU-PhaCh{L~{O|VL>!0_l0oXrz*sntzH}Y{e%0XmFMHX~JI6bL`+mg&mp`)?zJX&4_T#;e{hIUCuef>t+HX(Bdy;C53tPY~ z^ly=!i}}%*KZ)aWXu7e#-sLTR1~Zf4?c`lSEac>5{sMBP=s~}JL(jqFY9`*H%!4-d z3-;uZ6~cLG=-aZ7|DW!+k6Pzf`APVC&cBbV1aSi@fNou9`V~9i*ZE)neO!$|-ff>> z@hAF~X0~60X8n7(`aq1ALzjxt1G-$`WdD+XjjQ*;IAdSH;a6s4w#3f3^U!(z|BS1b zyeDb0$gik=zm7v!8o4{MwXfH&yBs5Nm3jSN?{V&(;$6-LzfLWW7*oOELvWd7nO{-R z_W&|`F(Pr5F>AzooaD^AJ=d?g3;b#ieLQfuhw*S9xa8Oy+?8R}KF&KR-mlySFF%sY zzhc*r$$pI{4%x8Dy2-DG;CTdd6LSpGgn^@mysNu-lNfXVG&!*w9sXo~Ec7fC^da(V zz;EPUavd3iP7+V@VJ!5`px*%=|6N@DSTCR%&m#QxXnQxVN<}UHc(I4Lq@vHM#-bpe3)jmUE-cwb_7uWjvwXe5d9mbK9X3kCj99Q_ssSD3{pC*bB^&dO za($%1uOeXfIC!40*sp=uJP)i50+$yU2f;yshtwcy%y{Ii8;T8s{4$U9YYe>8PXRB` zg)wgdM>jOVI=f^oI`8zWI5<3tK1Hyrei-+hewonih>f~D&9CfW?FzK@SHpV+*Pvg4 zO*!!4zk{pCa|3#lJ)mghLNHD(>-u%3*}ulsw2c8RuFm^Xbi6YfJT<1S);U-V-06;rtaiNJk!>WwhH%PX%x5$jc!O{fdbnqc)*-!8ZWz|A1~V z^Y(Fk0$oEJc6jM6;O1F>zA@+lewmxJwO`@rmml3yaeP4Td_rDqa8?gHZgHFo{U$5D z5+D9MxH>x`p!d9E7d@zNHDuFx9V$*m9sCe#*`}`@;VkR~xDM zL)P#eLUnRE8@Z7S-HXs4iv4R`^(f-2KFK7LFxeyIrOm((9~f4Ro<@`PnO%*M2kirBY+$?s z^oDk<8M^$>xT;YwT)Phil$0Jg=oEc<7?@4#*QyUbHIMT@<7(WAfF^bdXu)&*)|*pPi4F6uhIi}Kc| zWPkdUFBW`Sxc_`hZ^FXYLSc8L{>J z=vUUH*o1D?l7Z_~5x>*@;8z~}bOE`=(5>Vl>KOhQNUR4kuX}92hQ|ei#G`uw*3jha z;5_6GcGn^QhLR5x$cLBIvyt`4Y4R&EzCVB;7f17J8MJ3J5EE!SF*h2=Asknr(++&F zuBu;8(JeLlbiwCkn4bqd+o7ii`KvRLL%F~iG7f{eN673^i02!A`@r7+RIjF!mwjfi zuKF>ci*vEBI?q)2JKs^CX5R4qkKbR_pm(PDi5j;He1P-GA>_q*p9)>@=_l+9uCMBi z{BOR$YK+W%^ltKR+GC$u{o&KcQ$97k>C?D-KGmo;QP;r7fBpXI+@rwXzP}0~fBc`p z#sQyvhkW{U&ZoxE`mg!4F5g6@yBn_L=7{gF+yeu8#dlsEAJI$5o882+EHuAv^Qq^4 zY((zBvp#vzJGj0|{6o0nE{eca-7~~+4C?~&{!tRvQ0N;KDH$9{JH6=Q|PSoBk!RIuePNGL@^x1lX zfjy0g4{JC?)X%YxOo@>^g4&0+c;i;-x+e}12hj} z5+~?<$aHet2K|4BtIrj~)%i$3L%Rl4`yTze59@>=PVx7{`9AeO{omv2r?}yA^yXVJ z)|ty3e%X<;94vPJ9lO!*S7Mho^+c@&AOGyTzQ%l;cEQJU4gRRO)u)ncecFrMeP?{K z;*TdOCaP-Qzi@SpZx3&;i@?>Wt*mkKkb_{#J=Ld#3w-Lc3ZKEF>uR6$AzTwyg=kn_SsU7;Sm0<%Nb`&4{6 z@mof|tOR4|8N%_&3ZG&v_379WpK?O8wj zZ1Cw0zAM7qY>W=*wG^F}a~w}xith1g7_`;?Kwo5aB_Hc>d_bK38LrX~30HzxeB%({ z8HD|hT`TE7)Ve5Fe42od-XmkveQ*`?AHNrEUO!x&j`O`>G1dfo`S!7aUu`aeYxG;e z2#3x}zo>aDT$z6OdtB{LAMw3t&bvIrHSy~PxfmCDMeuE}O+H0O|Ao|m64%02;(56C zZ;tq0bk{ESXhsF}uwXz_Cy>+6eA?so>CjMoX!WVI-KTqIpBzS?-fRn(pKoEu{1~oZ zdFC(1{^7ngtO-W2egpq~z>&8dGMoF9vJ?5!1OIo$SKWMS$2X~`X9d)`INxQ)2xwzB zp5YSlY()(66X#p~eX87&+Sr!ZGa5plno+w8nCuad;RNem);j&`Q1{Q_2XMD*tWTyv zKIIsUp6FX*H1QwiQ_~^XIuJZ_y)U|@VI5KRA-up;KkmKi9--5P5k4(*fdlMqkFBR1 zK3xIZQv*Id1KYtm-*Syl+YRJ2b`5LglM$UN^y2pf=0Z2o=hFa=4>`7hZVP%PpmvzY zkW=VXyC?a@n1Wmjx~3oHQyKhO+~(6F<_Gg=6~`gS-OKU!Y5$0;62@??EFP{^2LkHe zFra)-dA6rVSEtT;R#Tgyu@Kvn@2Kzl!~fws z;mSZy(#UY-Od0%sI-vUH0(y$B{ir$V@#8CEnX4oE;*V>csR3=^)!3)uXH}SkAAT2gu3xK9wy(ObXFQfAVR537-ZP_UUy#;uAkyUD^k<_6@yg z4{Hx%UF5h=b!Sm;kTIkfeJl%oDYs84^7=F>KQ*TSxdgv-eB0~xu!cg;0kAlY*cC;m zR@JB_C5boVdRhEbiQZY!r;FvlXL<5iV6H6v_K{Cnm-{rWA6O|xtl=BveXNX6_3Qf7 zsR4Mag^xKOiJhfdPNC{6H9Q*wl(v!Rb?P4M0l9Nr86yYJh( zC|uQEfvIr;8TmdsWFdJ>U+H}gOwae}#d_8#=UMwbAqFP;$rkeezkJ``kls1GCVLXg zi7(GLw$s?YlzzSfe?$8l`P1b(`O}GK0$aF_@=bm4`}P6IG)-bX##+bLj^~>**l71@ z$#^hK9zN&XGKtuFeQMh(T!VUr%LP8B@_dkwRYbA3^p{0E?|-6y4Wzc!W4%(7oF^_D z@x?&mmIbU#D}}5V)HU{mT2Xgqhp`r5y}9g9pIR8nU2su35k8GW{>JoaXk75gF=JG4 zvp1kPtTi_T>l^E;PIsv{F8U8xtOi}&&t4^d?bYEoUM+p+RStfa`sCHmQG8$arZcb( z>rPHx0>4X|p-Udtsy`AB_>cVHReR=@<9tyJ;>B?TvhO8i{qu(P*&?3~_9kBnau3~V z#-h%_KQ%+~xRwCSaQ$O^av>RgEdzR?)5q526t*w?0UjT{nu1;v|McoN=tAM&HwnFw zQ62iV%o)!4jqge1@H)=(+dA%e?uSo%`BZ)g>jlo|CJ9%}%;Bo@Frb^3fC^C?Ez9_(zctT4tB_d% zKNkawImsn_yd9q{X51nc^_~Tki?vRS+U#9Fgs!AtY`*H&GV&k)dxL&|?A7OUUe!73 zRrix#tv~M73eIm(-?E*gAJPAE^V~BQ9bYshHpp7|%&SW$yz=bwYSSjKW^eUs!#=Nu z9P+B-F|XFL?p_mvcWmfq9LqW-u}`&#%^Ko1;H+0yc6ybDac8$zCmBtkEpf`LqnEro zf#2_f-Ov*F5o}C-j zUhTTfZ*)u!O_Rf36~(Ts$gm#qDg(4V;W3DNJ+aw`+{ACe(gm+Z!*k3Iua=-!8RmA! zH#IJLH4J{O(IFdrmLhWr^r3%vb^g3p^J|8y!;pyYMaSL`=*ql+ru;%q-ePZ{FVC(# zV{Qiv6V{Rc3+TOzdFCMoAAX}h@C;_pM9s+&u1W6$nlOhoRUy_nOXw{;9}QVU4>nU@ zr_jf#HKn|)&4@*}nbd3g?A0d$?YTvNVE;5*GydM^BhM7{&KJAzcNd-;TTsWTslG<6 zQ|gl29FtIkV(kdfe*)??C7{OOztSAuF@UMWtdBz4`V@zp3F65^jk)>2tLrzsDtDC_ z;jhj--+pSs+UPyc1e1759s1O(x~K6qehgje)$Pe%-JFV_7~LmQ8*&A7 zb}ahwOxB5-I~83j6ktso8(e{>Xmh=);Pz_#1g~Nnyc)`R3}}zd^y)}8a%C+#bp_}7 zkcXT(U`oWV@gnL-z^h_jujVnPbKYhWJmz`zD|)O#w>4RK{vbD263d2QA?9|kuFm)B z{S5Gn+*S*{Dm=@pfm6IXgsvNBQ)A%U1s}|~=T&9m{`7Y+4ox9++B4m&5zJY**sHD! zyxI!gbm;D*a{_cI$@z0Ic6E(cuen#!g?@IhhHi#my=n%ZE-S$o`h}tAO?do)o!8N) z${euG-#FdK5UwGQS&OnRibWp(K)tDGXI~~IIk}mBI+1l6b@&L!oKA8dtQ2AN@Y633 z2DJGYe@`@&ro`8yLYg!bzT~4)I*zcTgYO z2DA>D=fOu6)+`06_Y;dz2THLHWn?J=o`}WdywvuzV3cRnIYZeSsK~y~71qEtzh7^n?p{y?S=vtH=2N3VD`(gI94^d-V{1OyPY<+^7LLXS1KoTIZ988vYB|u1>GY_VsFK zORs7-^eRhZVh>IKK3=6_Tzf{{rsi1bV?%Cx^~)CG2G*Z}hYbU~dfVDdZg};o4Y}Xm ztGzwE$~1`jHH10jNb2WarF-Dj4DhrZyri;tm8%!mx=hzUJ09aL^VSXqYs1MoWUcGxRUEFrgXi^D;KSop{L$dC zqgNT5P`4PywqOalC(t>ln;rUGoQIAehw#}rcwOOlR3CeF+gSJ711ejPXXi)k#ZAM1 z#J2=JFAvyo4Q2fh@aY8gyKQ%JhT89>b`)oSs%`gx7P6;ch$8C^j>`0tTuhDDr*+jJ9iTEw}sHJV?7%hjlW@=K~ECj4;Oj}`W+c?Im?dPYAr{|D+-6rQox zfbj)h?LfcF#IR^Fu=peMQ`1M&({G`*a4iwEd&+y2^t@NzJzlvPw1-b6a8S z3dS+UU1a;SgYPEPCqbWsI-bg_ycy9GnG=~mz6?E*xs{Q38-5Q;VKcfmM(4c2`_y(T zec?)$fKu=0-2v;^ajYHU(~CMZ!{5}6@${l6BU$gX^C>gWC|5J%+cf0gAH1iE9#GDh zVB<2+&*ONX$9`;U8~u{H5%;}UMX2ACYxtCgx^td2YAbx(KAKNe7PIG0{*T(nUfg8% zF^dGh3j)U-*dN&G)y(zO@$2N_6EJcEpHh1g?q^L*EPnjeFV_m*LkwrHDjEBD4s@+T zPkQRrH2k-NoEc#BY7sfuyc2o#i&wcTdUX(A4Pd<;qcYF8H&|~D1BVZ(^TcZuHU30h zY6E`H%shW0ay&Y?459vi@@ULUk4neqo$d&p{WG%0J&7+J)QaioiHxfFe&J`2`abaJ zxBDJ#f8tTJ_a0q{LtTjFmAMelcl4EG*m2V3)miXc6D-aHM`Ph_zwFW8iyo!9=2192 zhJWxVXKeC5o>%v%X*p)m-==}bc3>zMb?dE1oo;(HnE5?#dUWTNhrc8BXc*VGMDeO0 zdLKuAIWsvyy=e;uvL>WIGN;mGkD{VqBaX>ld$bc78{(4>=z0rU$Dds1o+C4P64R^g z*i-GiM}eyz)#rM5_(qRPpT|Gzxqmepxy$boTwez51bFsm?>7efMW@)KI?I0U)uhw_ z*448b)B9Ne-}~&<8L)J#4f_=2_H)+!$B9MtD`3ApJ;BR6cpp85wQR~Cz|?GTlYzDA zD%O$P>A@f9Da0WYYsaZ~y?S#7J$I8!Wq1eLigg(KR3&rq9$}A9-FvYPeopRmrib(b z6U*rZ@W{WKoSyAfed5uHd%N=Sj=nt4aIB}wE?_S$J3Z;LS0VT?De;Y+lm1YR`Y-TA zE^W@}RrDBO^oBv0nzr4e z#A`juFqdbB)I3LkjkzYTS~jO2wIwE*z!to_uJ`EeVvm}y@aXby9?jSWKHyYD!_C2M@@x^gG9wE07F{-=+biU} zU*^#zDsd$>`9KQ4EYuMgCG~E+Q+@Ofbv!0m!I~Ph#!?e+N8z z%K0w*)0umx7;~Bb8@ZdX7i*N_yj!@&e$N#4AZp>i+pH~?c~t;^oB=0ON>Z<=HD`%m z^|j!77B%=G{V)~ZuCdp&oApCg)?aD!um_0mlC}40CH1%53F^gTYWi_%Cj6oi$HWG& z)_|jf!J2WEb@_7MU9c`+_$SXHW2iUyU;y~2hEJvwzhT6^b$9AUBd->Knb>RDOW()- z9KEGBb)dv%dK^A4MNimv$fHRIJ<1ZyDe`F-d3=^QwAky>?O&-MJU?XY%DOF-b=o}Y zX?5y0vHD0JKg7q;@SR~YehPTB(dAJyyGMh@dNgN%N1IsZ4cx*Sh`!pPGQ9fIqO=B2KZ1X7QyiOG_VTD#TWTA!4ly#Jb4B!Qf-EH@XcX*_NGeIEY?|jse#d3P-}8iNS+@b&P z`N**rtVf7*K4`mAFJ^*|kiEzaao)(jp!VXmhHJ*Xr#3>~_p@u*cs>Q6zAb$A92CEut+mB9Qe^!mWPVVOO8 z#JtQL_i}A>QIC3LqbDZt$Q6fNN{%1TdeqPF(ZRalGd+4H;yf+>&*@RktR5{wc1m>U z$Fy)1G+rL4@1YZCfLIB)bH$<*x9?o zFGb4moJsAUcF(I6tfSh~Pu^T5?&R?St`|7x(SxY;iU#bHfX{;DTQX|#Htvm0Kt1|I zE#Ad_YGL6c#A*h;vKFy^MSQ*ZdN8;rS7yKTXf@Y%aekZFAOC}xuk>ijLVUc2XJT|c zS_+?YFEREX0251oB%d0Bg~ni{9eyUq2DSBQeNFtG+oO9%)*gL$Zpw{3__kVu4`x#1 z$!|kNdS6WZ`qZr(FWtKT+^r{X-5UDPt%cXz+E|mdc|z(g`u99cy{AtdWt0KOwTNf5 zTW-C&;8x9ZZe6|T*4dkGJ?C2edu|oKO&x-+1^9}>aUHf?$JQxzzymSM^VzLWuiR?& z$W48B%XHnXIgG(~-CAG3tKB77JE!LPU|&RCI#Pfh78@R~-TM87Tle0$b>L68dZ70S zWcNMe){9GS{ZW>+J2|junMWO3Q+tT{EBOC`ZlBR}^;Ng#q2D>=6ym(%QMZolb8FKP zw-Vn6gNr>1#g7HS+4~#lcGfM&X}7wZaBI*pw{A1fddRI2d)->F&8?V>#rxgbN{o)| zByXLpW57!Y>%GHU=_5<<+Y3e&o*}!j)}#knbFo+Pfx3r}5*{P>+fma7;AeVJn(AJa z2LlVk=?7uxfKI9NQ@4Y0r{+Jo3r6-rv(}>`%RQ=ttdoqf#prje8_R7051m<)6ead1 z=AEH7ZwDs}S$~k9UarL$L+vO2@8NeL4bL;5S%dsepXq{5*~#5F^f_{LcvJG8nsJ1j zN}1B5*W^{|Bp#i^=MAyxJ-R#tPtH-u80670dO?Dg#Hk#4h`&Fqa?8A!*v{cN+pVWQ zw<;MJCy77$r$lBry|NWP+J}CZx`Jc)_gw2%{xG+G9_Ci9!ETN2?N-O0ZpGzTVYWxH z_p|PLf=|i2TJSJUK!>XIy!iCGKioRD!mWM_-I~Cd;dQI(c;Yv}t!MarKpfT`__rZG zN$RIQHK6t&^D5YTu?Jl@yLD!(TW=Zn7PxiOzW zgpKi5yOn&sTd6m=^&b7hk==fhTcxdTMUBPY0lrGMfqG28o0J|s!J=g@Se{DGEd_&X zz$Em$*MX_!Ze7G**QU7jk@`_619*AlQ7-yL>0Tb)$G=y}qnLB46JW^l+N0%nSbt#C z6gx3M#;8u@^=a1r_*#rK^XRX{b13z&Vr7rgGxzla>I8bV+(kdY7a55EaQra<+ZGa& zQ?opZ6Y%H;^*JnyN4?3Hsc|DPU4s}Tm_#4r-edHcnHL^l=Sm)rYLE8lE;V9Z2YPwV zpd51avResbl8a!)%Kcd2vl+a~-iPNsw_H!$@=y>9;|1` z%WWy>naADAJI}2gMz=D6kEBD~$^x$5KL4Z2g3u;Ibw5-aSAb>~Jd)*vj&&n|&)V9i19U-#QGwRy?Ig zuAsKIXDvnFd-?)Q;m>CH^*a9Fg$@^~Upv6(CFmb@rLXqkxu**2+gQYn8hRZ5KT#{! z;(IqfG{LK7XVyG*spF~n{m!kW#4Dj#&r%mwWn(??)~&SI_EW&E<}v7FJ@FUyrOHrh zX(#$cMfycT)&l6$m|DL#IW>iOvpJ83yjbIiJi+0|j`S$jHm#C)G#$Fx z=s)LAx2k}@cc+lE-7UY*&Ht;*tuS)Nl!-ORENZEhbtAfEhb}I*4@TF0AKi*h-P(ii z+x_m=_+|JWeS1*%hN{|65DzXx3-bH#gS71epQNqEpUAn8OzM%z)bKo zn_M$^^r_x;2nqes;7+?V)KipFG}!PY$xiT7wUVQSZpP zh4kua^rT=9TZWzkcas-j%sGu`#N{5n*yho-v($xK$UE=RP5NRy;{G#v)d_n(k$ZcH z{Tuu`{2FQ6al)=BE&A6EK$FSklm zajRfb@RplgqmQfs17n_of9%csnBH>5t=8}{&R`AVg%%8r?!w_FP3a4X{( z`c7%$o15nxFnluxaf3&;!>m^}QPIa6h(-@%en;p+3b?hsK7D}rodq+=TY*FTyt*yV;#H|B1z4kIrf0zO zTpka5hTJQRe|o!hidZM6Zykz553K}_`-6MxPCa<^!`F?#_ssaveI}1NMxzGi^td(b z0(C2dwI?-34XIt=a4mE#+jz7Fx^+oC8bO|U!E=xEZr!@!)@$PZM^rGE9o&M;w8(IQ z<>;f3L*HoEG-9oO>8)EU@ZG5c^los`>V#X}z(_DJ_mVf2z-F+|3<#qav#yvukp9tt zXIyNY^xCb*=ZG0}>!6mPs|`6&4u3wPHyi}hm7J{2q5m074f+{8(qoq3(=+JU z`z-#a?lsy)-CT^^6!g^3E_FWZQtg#4^_l5X)SWKv|I?)^aljy0xtNHx=a1C%EN-<; zPTx=LriZvy{)0=E@3>T9lS{+>E@d0!Qk?@X9V&s(tJ0tAP}3WN&o;!rqg!Pfu%@fT z8i4yJkGW*u=F;&wE{z`G(zOmQ6@XXjQTPVz9Ra^Bz_oK0n3{;rW8AX!W$oOa`V(-e zpWUTW4wsTzTpGI%+M47SIl7g6i9)Wt+w0cK-`(m!-t_|qm*Lk4?8WKs(oE>mp>xQ6 zmsX?C>8TOx*v?nM9Pyk-T=#>Urbpcxwau*roAJRK*3~nx^My+}iy{MEV&T6X#Ng&L zw^H86PDYMf;20gM9HKr#w`)B%P&eXbz@`DDLre9P@Yxa;8(C@K;M-Mm_Lbi8TF!17=5riYxqtsg;jIOnZ~6|QCun> z-=(}2Tsqv@r3^h?G7$4!9bKx&@8SJjGL3iXBiLIB_O8})sZnugLtT1*$Eh@ro&4>F zOXIk2o9NPDFq3wXOTVvi=_PnO3YLzYbLqG9F5SuQ;(K$KetqXuy$eoN`s`E}t4k9e zxb*r4n119^i%|4S%o-;RwG}-3INuu0t%EO|n)$$~<{zD^`GZUIhP#yGq)TbC(F@6w zi>cizTns!=lNuLt>q;)_966DI>jMuuwF#LwtGcu(%%z{uXG;`fR+^YIH!YaYRL!j> za;sEz&dY!g>Qt@lZuYBPD%{JZVe4JW^4g^%>A)ns7c^$gU6&f)l(@8Zt9o;&6#Nbd5urUxD`yjUieXzb;W6>vG9&$0QTe*_4 zj)u>=%IMh1tv|`FRfFl77V-xSBrws}`?7v)=9ar0&!Eu7PVZLPl+Z$BL$7|!e>k2x zXh)}+Zq1zGR!=aP)!^2W&S1YL>qzWegAd;#)0vd#!=BVVcn_Y+bKo5OxRPG7%B?MP zSvv;s6+Fwr`*L~mC_m57_;@oiU&VH->oTYMI-H8v5nuOoD)}s@_8oTW{57ZYfR)i# zof@*!sh69b3j4#U%`crwo6V)@ja-T~(W#81or>1VsTft9>SiO>Pn=p1-K8=goND#n zsie$rOdNBjcIiwJuvpin^WZcv+^JDboP5XYRG&spjoai@Trg6UIi8#@-D3DFyOg`W zOPjj6lz5a&KZCnZ)S4{yoVs1jsUv;C;(DiY6muzGMVG3RBmEn?RPz^dVu(v4pt&5Z zX_KhO)4=Rhm&Vt2DrAI{@4KDqml?jzUFuZNrS2nLy3m*U30(uHi@)1~et}Cf7Q6Ih zJ~eKNOBH=iSzn`bX|Rk9m-;Y=Ivd~aQcP$*B4-kLRAjA7v$wdkk>i7TE-i7n*pG6l zbQkV*b*YfqrD|Y$i@~Kh;LWHNtopw%u_X?NJqDQrT=)t)B2iNao*E4hj z=LtFfafKY&>(cWjF4e#WBYrkp(D#%}KiCF+1y{s8?|7#knw;9!)v4uNf0W&+tfk2H zPWW|(ll81qOP@Kk9J&OD9Ey3*q0Zfj6~13!cIp%K$(cL1l2cL2Iu#0bnh?*tv%w!Y zU3ASM^G1hSLm!^esa#w?G}EbhZl`h^oH8P}4D?HSJ2f0V@-BC3;Q{!A^VO#vI=9QA z&xw#>aq8!@U}Y7uRvKC)csxa2Z6KH(bmvEf6fuC#V)BK*6PW9g4Mb&NSi=2qR{R0_t) z9Zuas)^F6(JLJ;dk6<3!v{#(!O`ZJV47%dClwi6(xqAZnBe|Xq{U6O)cT3OLLF!c z7BW@mScO_b?B@UBP@mr%+PL1KcTXM4mD{Nm%=ys_CI&dve~v>R-#QdU^fRtMD2V;w zu-|hBfAiqb-PsOpTjo&Ds}B7d7hI)rs;$+bRoxuQFw3C^TzdlULd)Xc#7>Qm=F~ax zR&%98{^bt!-h*yncz8mmnx%JY{X+D0IrQ|1L%Yg3Rk0;l0sEUmotpH@p+;vNI(ERJ zha7jJ!%1uy58a?Fybmws7{ql0k)Vo zHkVVqGCS3vC|E7d{8IRip={t8ebdHts!epKUdO%Ok8`l+6mkzDw`N198WmwaylUiiDi(8_ zL2u#y#F9?+s6qbVzu3(CO#Dis+sMLB)k24ORh+t3#;F2~z3|Ul+Q~a8r&cy_>Tz4A z99_t@j_BN!JgMbWe(t5K=+y4==nTJ;jh(8%Juf~U&slVALi8L{!aDmO8vpM zjm-P4KJ}pH_v#ICPO5cz}$F=$~mTI?Y87 zi$jl?^DA_#kT<<4viN-jIlcNil(myX^|*G5`F}2TX!kUS)_NUkgSL1`{L~Adoah4$Hq7B(>TM2rd=4cV=Fm*?INBhGwznr&S~_&OvqQNDI+SXHLv9m& zW2{3Xu(efJ_;GLE3(CQL!+TDh)%tSej@+`#bIGn47n#fG zebTOTTkLvx-mX@U?b-^@?kOB<9ts{l*_D?0JC52_bUX8Q*y%BLeL7%Qp}lsUSjTa$ zT_<1IwJip=#dfI7XS=L-?D}xbuCPsZWn0N{nO&(i;}i5d4sAHRJ}tHD-88#$M92PD zc2&G#*RSWef5fhO8|<3D*sdna?Xn;v7c{%DD;397>+RY;&(7af*)`;YT~E*0m3^mO z@3+~Nk#T;NUBAKm6S9ryI`6DqIhm7tpIu&f-Qr%}CD?+k$+5lf9=qBw#_qRk&Q^T4 z+pburu@fJSyKL8HYDb;$h_zqBAurx;Vw`y4WUzYc$Ve+>JF6USxn zPk}vkvHK0j>iljIle!Omcj7VfrCr6J+jW~`yw~u0YS%2{-t-DG;aP`K@}gZ6ui4ca zc~>*o)iR4+74q3tr7@!qyvN%WKY;wncJ-e{d}m7%@m|*N@O| zg#LA9yYe@+%iY$li|z4Y3%mAk{;?ae>}yxQfnZ}Wz8}K9{&poug&%X^?)kwuDQjKU(~LI%v}ec7&+}) zfldy_Ew1H*K3^5PN|Ytu`Ry8(#;&T+`~uC;f_Cl9ZdV9;TuDsK;@VX?)UIeL!7IF) zL*pxHS33OinK{d1*;Pbf1wM6B*mXIHU6XzQQ_xL_3OziUWwdKh5xW{=j}`eZ@XgA2 zVC0icrK{N$9lm$qpC>*zN^VyG9t{(q6ZQo4%YnRd*q#%=wC8@bnBefUO*uN)RSEi0 z@GOnpfx^TO-TjO$*z*9odnNEq9{h*xofF#08}f^B316>hXQvKhcO5VSpD=72Q~{r3 z&+O86EyWl6(KTi=bWemG*!2YY7l_sCzT{MQVn!?%{bJYn`gV08PWhmJjvm#B{bu}; z3fc-B6Q%=e*pp$fO`C?<6vJ&}{b|#NMK+zEXVcS#HXU1L)8;KUMLTX&i5oWcd1TYH zCpNu&YGW;dkA~QEG0dj%DvvEB?xZL$Vzs=LajH2ZBDbp)P=ZEASPrWdbKdZso36qqfPnN*pzsIO)ac8or3mM8*FX~ z%>bJgkFcp2dhB-CbRYZUFR>|v`HPVMg!5E0vD;(Q{we5!opqVt1DO`5O|clo;aP-x zi+_a&GIHVf-g9mGg=-tQcNKr6#Sbl3*i>eYO(hwF7u%G7mQ4eu+q81FO|v-f%D5in zLmXR8AYMalDu-{9ZnEhEcHF?8^BZmIxYnj{?0bi-Ch!`E&PRM)Ck{sZdaw_^?qSpL zZZ<7r{%YcvY$vhhxEVcOue52~eC(WRV|i&)fLINMrgLYT`nE@A2b+HFgirTdb?dxU z=^k387gim4U{!`2oHMT7w`%%3tJ);Bk=x*)v`wjN*)*&<_T$sQUaMwau zD#TcL$*QfVta^FVs+^y#nw`leXL)?k(58aOT-VR0ImC7Wv~8Hz_X_in>w0Qc(rZ>( zPgqsyu2p3cV}D-oUKJlT0>5o+I^GjJV0*cJR;Af(<$ak|F^*c5=&Y4@`22?7?@z4S zk_!Ex%~KY?R!Dj^Rorz}CHcdu&B%>^$Ev7NZOW3GnCGx5 zV-fsQida_w%jj_WXPcU#vtgr^|2LUcnKxLq;+$1`@zKGTR^5+f)59dl`VsrGQlIkJ zw4)&LC~DIucr8Xwts_?5Mc=c~U3!k~_-oWhtA;^)F%*A7A1wvAPRn^lV$Rr@hJ41Z zf|spoiBDfcSAdvYes5LY5Sx5a$Qy<+8hDLvQ}Wo@%6J%qT>50yjFVRFfOZZ(t%`4w zys|12^kd$l5As*w`#Jdjz+^xwn}Uioeou zeH`(xdD^NH%sqsC^AA~75=`Z|XHk=AR+Wo`t+B1zm&mI9$*uaB8qB1}XSuCfTF$Ds zjjih6%c_B+t(t5FcT=tE@zA2B(ZSFUR>es}Y|>iU!?S8u3gVmHs(2N!shL%;dRz5k zjFr6&t1JPliqEsMZ(`wZ`K@Xg%0OnXq*nb5?dWvaQ~-JP@JTnT!iHM4dxBN-pzQ~3 zfmv2%e`(S3j}}#Ui~M&Moe8n3H#}ly2Gc*`vxZh(=>&fISru!fRqMj6s_w)`6VdCj zMa9s2%WaD~phJ6Tw3)#(+?px@ma&D*oLg==vyV0RZe`g?MJK9LTfB; z)f0Sm@n@@c)g>33SXHz=K1PSxeZVep`9O@OMj>`_t@;&z2k>|K99EUe2gVD76?kMv z|7KOd1w6d)*wzBSAhQ#6T{!lEM}~O#GdZzF_VWy23ZHe)N?dbttq}1qWz}P$cU7x8 ze&R06FmHuMT|HgO{GzbmECw6JlAkEP+M+ zl3LU$lSLQvTU5G`MKOz8w6(fL*V_}nVHRZ>Z&3}CMIMJmjVD{w^|e`-;#ia`gGCc_ zS@Z$AjL_vNX5s${WYOYA@akpJl2I1+ge?lQTa?0Q(V<^0a(yzZPeO~LWU=UGHjA!i zM`i(wj+TT6e?d{DsYPr1z+<#UcP$o;c3HGH+@ej;)`$XKa*HPD*m|<6MT486 zZ+DBf4YkOQy*H7)7ukiNe~|(or?RL3G>;jXp(~x!qO;KYp|v-`j?NZU>u1ps=ogK( zs1))iV0QuZ%ZHus@YNdVV&Sju1+f#I>;DX|Uo0Ba)}p*!E&9DLdJnT`+8B#k8qgoQ zXZUFjqg{53%EPNb5sNxQd%Ozr>R2?pu|?flS=0ylv3gtda1cC3VjuGlLEi@2Q;hfM z`;ZvrEJ;ktTNJ;tMeC7YtDZ%fn&ICL{O)Oyr9VDL$9K@rg|2f}i!wl)C9g$0h*QF1 z7WIWk4|w#gYSBV;=!f4wwLv#@i2l)}IPXmw5Yw!8iOs5-!K}~u&FWRatfnQ*x?LaK z^)a)bZ`KNzS$jQZm1jH-H>+J7vnE3~J)T)Rvzp~7Y}V^yW|b~$*2Su3{n5&-dqd2s zZZYc+G`|JFA#{6Ym|63h6)UA#TQi$wDsI+x#?jJdb*o}#PaHlS&00ImtP9X?hHlJc zvuaH@EB73;lE(*=Da?wN)2zfr%xaC?X2`7ivsteinl+=lS;K~#mD-9tzgd4yGOH9a zJI%tDgl4@>ZdST1W_hqPMNzYS<;*Hq)2vm^%<9(*IV14_az~)^YGijucFd_}HOOUV zO>I_Vj){wzwG5dKWX5fTjcv`U)E9Z9%nCCi1KGv!*Pm`P@5HgMlv$af`ST~UYC|^- z|J15ymcOZ4X*-y81=_+RkTK4zC(wsL-_?$P;jxeT&&rtfusk+aGV51luB(r(&CR-w zpQ;d_I{3K^vWt&5Yg-s{%w|=Cz7hV+3C&@d`ECO`>>SpB*h1SIxgC0#b-zEchrT;@ zFGao^`6n!9HK}D*M&^I20UhUE(YG|VZpBX(+ncq#8?yVEb!!kYz~9@)m{os*S)s_T zm%yYFNli+S)}#t)Olp(Dq+QufG88pwR5g>9wKb{nNRu*#n{x^HwJQ z*3rcO)51j0g?_S0QRkT0>oTdCOqxwxHjgl=rp2T=9+TojcL#ZX=(-`V4*Eu$!@Um%T}xq9)O<#zEo0QPB1TOv zYgFIbMipvq)V*Ft6%8{g^Gu`qZ#AmPDWm#aHER1cqq4s?sBQwIj%6|GNKvDb6*sC@ zd7}o`H)>idqwe%EYL10_zjAGhQSnb06$0JC+eWSVWYDMBM!9ps_a~#8L)WOXQOTPb z6{WLL`T85R6xy-KJ-LzV$C!J?sDXDmCN?TT2BUVCGU@;{fBtM#;u=PcX=7BH9!A9& zY*brlKTJ0&%Wp=_+>Pv$Mzy-kJnSrktR>htys=Snni}<@p;6D<8&wW{&y6%{l*6bl zGmKik44aUhW)FTjY}8a_t!Q9W%|__d!l=t_je3vF72S>cG!R|GjN0NgDkb*17U8#5 zMs-?mR2%$eM#reg8}*A()u74N!Kg>r*$zABjWVjF(sGCEKYK+Xrext(BxA}Z*f_5x=zeM)Db=*VO<;Y9i z4LW>N9vgjwjH-gn%T^=*k6wH_+o+2Rjamn7eP}a4dl_0Q_Vwy+WdGHuJlGclc_W7# zb$+~2mE6P_eT$&4585cu7J+v6N~3yW^Q?DaniFDBr}74E?qX1fVFvx~HYhOFpwa<@ zl1w&e#3F-w?lvgHeS?mHg@S2~x|_}@N2o!oQW$iyx(PJkHpv5B%`T@D478=xNqe12)2Hi*Qh`R=Te#wXmjvE@Z zq^m(k8Cl^`7Wuv4b7%~@;HwSG4LZEbpf{%ts)M~h+&1u?yFr)U8)X04pr^lJTU&$9 z^f0J?AMV5B+62ZV=+W1Ez@Qmt4O#J$b_za6ob0OGiX*;gO-&usAE&` z+XXsC#!F#3dNxdJ4u$FSu`rc?7N*}nhspafOlFRoKZNN`Y=f*>;DOvpE%6O_m_$5x z+zwNoYhkK;B}|vmw;uCHyb9B+M`4NrUDBuqrAvg~=$u&Kt(`$D!QKBy(pkqhef{5m zxDFi*1`K!iVFUW4)P<~23MJINOpA0rDHE$lL@G)VJL;w`1x zl;7nf>70C4?g8?wiFkKO^Fs0V685Y?*KK$q*``7QErnzh%Cq@S!j>t=Elw(^MwyqL zx;MZ{)(a;EB~%NIk?P+SlB)4{#aPG))wQ*16fD2oM>tuZ;bf}C$>MbRmgMBF)5)jb zowS>vI{)Zo#YiW=7dlz!aPnV)lUI38Y{IlMJDHW@#E~y=vz$Cv4BJ>IqlI_jf0l+RPR7m> z_Z%k|W;+=)!^uu*ZY}J3wVUETAlw~Y7XgY{PuMGCoUEMgq^mrcvqocmy~cprs?|;= zM>|<3TzsmNY0}(K*ga|&h2JiHkHv9E{k;BmGUZ>5^&?Kw4@uV^C&gWzl#X$-QMoUZ zPu{ss+RK|}%4xqeZd&EUvct(C)u5?r@aGLD^R7DCt?TFe*5VkdF(lmXSSO9MogDwk zi9y$7_a#n!rzZaWP6le+q-o5hK9R1+(s)Pp?CPY&KqtS3Ikl#O>SI+sMk$}EPEIV9 zUe*1aIID}V@Bc8m#wMt>QH*OnrOQ`-hB!%3eTxY@TfWYcuU*A!*{=776UybLlOF27 z?O_T7Hf=tAma^ z9L!bo+2-KkMh8>&ItV)LAWSXrpo1Pe9b8}M&^qA`p3QaOHPgYJX%5=yT=;5-<_A0I zx7)$U{SK<0au9sUL96o)${ll1{(ytHT@H$Ga!|0!f$0wi(Tf~BS?R#4WB3{ehJ6k~ zk2*MU(!t~l|JQm6UwF>JFJ~O2A9hewF@E1H4(a<&F>XprqxBBj|Kq?RjV|ITC+?uD z4(i-?Q2nt3&nFIycN{#Go@#n+ia0vYc96Nk!8l=$Npn$YKBHz(u2t3d-AjkouhbYS zsc}`z$&Vi#+<)fahX)RR{LP_zLLID-_9N0!Qa;6rcdWFQ72o;K;`vhVeP3zZ)Nm4^ zW3HaJEv0LGq=O;yuE#>@5dMqp4tk5%OWKwx{#bD(RB`gxx2jiTCkGp;&h?#CuBEYI zbI|g4c`xms#lK$M<>Xnoyqc~Y4+y)knv>*)PO@7#dDF_tQXLm6&N!=sqvPcLEC;)n zI+(vsag_IhvkvSJ)r9@Cy8h;0oSJjv)EW`GZiRJpkY~bw)?>@+0<9TSz|$QCe6h2D5xWZ5cCdis=L&GY zq~qBFt^ZWO)_?T)?*iIwQ6IH3>U(2x0r|@cShul&xZMS`JyL*=`ft8bz@N7ZSn*#0 zYpxd1O0j-DTEK~Y1-w=Kr8)*IDB$Y+0zB3hFl}!EjgJ>FRoLgkt$(8PPYby9tbjp! zyiMn(UKG#i0**Q$UjRb{BUe@i|`#|GogP5*nAK9V8T!pL*W% zRRJB;cNXXP0F>~^8$V;qVZix*HcXgv9(oywL(2E@{NNH6AHK| z%{Fmw5ch83n@IaF()9h~0tOa!;QEyV-v%1ftsLaHcTm5xgF)SNt&J)mQQY-r$cLo` z4A~&hwinRlL;(lx6|nMi0bdIHwy}eMIylJdsWH>f!CFrT!}1EaB>cij%1iua;phET zz&D2r_^h0tDL;=Y4$Q*7>Zmc$+rdG12c!KQ`ai4y7iR&l$1CsYiX-2jNc#lkJno7- zd0jxpmkzS(JMa+iW&>jZ`jIv7e80np?x{G_a#>?NTq2YW1VX95{k`5x| z&BB%rK7HrlJq`|e>pGZfXWRliIm_&9R0~>Vr}#!YxAxd|e}|oN$L%yaWvA+CJ74O! zR4weBoif+$Wc+SttML7V4_jkr zXs7)&J8y*jdA-`-cH;Ni^?B3Id|@s=w6o`xogE+Tnww_F`c5(5*!lOpokzde*(cs9 z!n*!xr{X3%`P=QBKdRS-9sSJCsiGR!l?wP+y@1g*Fys1+_Z@YB;YUkVOin++n z?iI>)gPouLwbSv6ol8&cR4OIkYYNk}fdAU-8tJC%sBZxc&30lPc3O@WX0o0B^X&9o zspmWF)H`FRmNfrUT=f@MXzK#Pdlg{vDq!|t>4>w_Dcw#%o}K2>zj%_J;Q8`R+!y6@ zFZr`yd2W&387(wkdKQr9Tfkgn0S$)AlPEhy(o_eV9iN}=44Pu6i?qA%k^l0#Kwe#} zP@wlL@pmbpn7hVOV1ecf*{Nxim*Sr;d=smko8#=P7xy*QdB-uuxF5#>wJ~CbP46Q%+N`v3WuuLuyKVR$v9bKDo?o_6>^~d--L(jr3 zcCt---q<+4#Ks%p7HqOHZ;y?DV>Vnb+4y`@@gCUd^Vr6}!hQS7rZur`R1^0fzuIUt z*T$HIHf}AqX}+@!pZzxOpO%)}HqxKlv<|wBv7c?k7nK(!?NlCaW0qO`P8$`ZefV!S zvgX-XvcX2feZriwvGtyf>cSlezIYlY2%yKHobS* z*r|NurFo=uRu^YT3yq^5cHZ|Au8)mgzVaZ{M&EdOm}O(%7#r)Q{SSHl=BSO5;{NiT zjY4r-zqOOq-p=QyHa4}laiFjK_EWtE+jK9ijmx?6N%@!lLpg1?5uv+!Ui6)Vmxo9fpG61S{YEr%FKpVigebox0M6RtVF^W4&T&t|K-Dzd_ zDJxgSz3hHI!>;Eu{&PNuYAKfnRx*`;uJXS+$jUqMhvZ71@NdMwO8n04R=$(pY3uSC zx;G!&gM4O{v@)rJe5h-sn*8tOV&$#;Yb*XD@_xifD;p@<>|53`}Vw5ywSfS90^+PV3tK_jstw!}cPISs+aX=nz z6Y>}_Dvv$u@_2GQkBP7IaBq-LC9Sv-7@kk>ynLSQ%w^ZfTqfMfrQPRTntzqYndW(f z_RphuL>@_#)%N9S-@iQSSI(zH=X|0E<}*DdpI*P`5SPm^K~xn?Q)sqoy+QwTs9QuVqBEV?j5)&CFx?vm9O*$>pK&yISNDs~ip`s8)_#?oH2S)!JO_hjUpe|BIHE*Uj>% zCJmEg@>ujFN1rQmaQ~dcj4HVVw9aL)^uGy~26HY~M&+_Y9^R7w6VByoO`cpLs^w8u z8ipRr#TLFR&!)SMtp{bZLot4`W@A~DP3(y_ugyc|x9$mT$= zY=$<;W^ILRs@KY9fMSFS@1mHk;<9-%DVvd-vUzwVTYnF7nBG;~@i{m;W#ep@jlFp` zjXq_OS~{ENm9tq`Cma9v*_t!0xLMhZ7rx}WY~2qbf4<0}W}6(^mCGhwoWC^4rs&Ho zYQN8-$j2<27R%;k)ogkxZhhffDgTyZvzfj;n@We}=c{bm3sb&sHuuY9^WVcP{=AdL zlv`Q){w<3~igl!HHZ;wq57HcxO-1Fue|EOki_7L|+iW&h%;tpp-M^7Vi*s2_KAT04 z3t5`ylEoawODK}ftQy%o?vPCf|7`v;W#ge5*6S;8mD{eO+5C1di|xX8IGIIzwXu49 z`f3*2gzxq_i@++`43vg$eX?mdJezkvD97rG|JXvW{T2>Rvk;tXp=G9pbw65k51@s% zdo7%KV4-xiEJAx_X%1Hw1IK1*9fvGppIUgg)xy-d7W!mc2p?*pxSxgYgDjdWV&U&X z3y)V?$USd?Qd!LIl%?Njv+(PgMeo-ZMl7-5?ywLt%tCx;@s_pl=_?EUnyR^2v<|yP z_i|e3@{ffX?<_29DPF%Ut#NDN-f9b;RtsZBSZLMN!u)HQe04pO>5npr{hZ0|h87MF zuuw(uAFr1#T4fy(<)i!AznnMKJl7V4WUY&TdqUe1F3VkVQeX0mX-u-h}ac|4P)cQZLw!a~)S z(&?YYeBld=W-(%;1;-Q%55g^6YHp#;`%LmrWfHhIlk&U8e;|{6r!%>BJCiwISnwT| zMfp}5ztUXfn1!PYEtFM0F23@zkvuJ;Jd|tUf0?*n$<+GCnQXe8$xp&>7@9@rc3HX~ zB}?niSlGJB!g7s|*%}{f11;R`t{OM7(Ciy|R6%u;=N+D9Vps0Jd~G3Hy4+@GGSZSs zyYBMyg_-F6W=j8M)}9$=+H5kjeV>^|XUw=fG!s}NQ}@&;clq{G{66zCd77p?JTv*N zVkWiEnVGiPOwFlg{auj7O504OX+}=9?M1-mH1X7EG@)S#~H>dwpb5aeO9sz4dsn znVtn^tpg}*OS9%3n7Po!%o=wy$HL7F$uo1LtwsOKTi9_!V^Cw@_ZgXdh|Ofj12gGA zDo^$Op_-X>rIkZjGd=Y9ZYwjhbUt3Sm|f4pRgI$_s$cBQz<=i@}xY)~c*D(E`cWhU=uGuk+j zN&U?FKFQ1uX+I}yYmNJ{%`60I{CYghBvRMR(Pf#`vS%_`^*W<^{eIP~HD%1anP_Hq zqM1(8(exWLm3wFm*Rb$hV{fpq{iVC|&P=w>&1AkglY3qoi>le}l9_D3XC_Ae&zNH7 zRfL%j0cNcMrdG*};bR7`uV?URNd}QA8MN$|!TNd`oGP6`w-@Q!b0?jN7t;wppU&gc z>CEbDrf*j>x23_NIkNGlo6eFm=}h05&V#?wsk|zk zu^lvKTFA$yX2#VJUdQtCmRTQ*>536CCNR6z>bwendqx z`O0}i1;tWptvbwpAidi&v{yj}Tf8z@RwaYl%5}-obf%0H&Hvx8!&S{J6yF5lnhG~?Zw9^AW-xkQ z20;@uC@IhHt8V^3s0knPI9==So0;0pOj&vS`gH~tVgEj!L0RRQ|A)AKSG|7HIa>x- zE%GcegB8Lf{G=!|w|bf>T2t=>?=x6^GedjnWDu(QmY$=r@pA@#*%|zhnn7T4hVBtk z-0%!ad1tUcARW)9=@faF#!na0Sa39rGyBpQxjKypQ_^T=Porp98k_s3aiD%0-Abfs z&EygMb7};E9n$&h%XHG8r0Ms{G_ua5={v48?Ej=;+>l1Yg^J@yqj;cRZ=S~5FVZM; zWrRKxkDy1bbQZl&!|PfahlJ^NJdO9~(uld3#y)8{xJ$U@X;jD)uX`FBs-*Gi+6b*} zIf5q@(n)!q#=Wy?w362HTcvY%8YNCB{;@RujZ4!#qiOshew#Us`aRP4_;CbP_m9A* zOgfPdl$-kftUj&RNZZmhO6h3%GmV8C(|E0TkEOrjtTg^jPUCFHG=g4=w?;bU9;PvF ze;SF~(=>M=jo_JS{4yzxAAU`vvW~~+q;Y$R`2I-KJyvN1CZ=&m_}=x>St{O1!X}8r zJTHxsHKS28oBDX zFJ0PHn|p&5Ye*VXqS9z$P9ywBJs+D!K|vZ}4bri^O4I%XXrxE#-vfhB7OO3Txph04UG$L@%CSqM!nzCSgrBkFHdsBH>aP*LYFk% z^aX+t}%EB+C;N3Xhaa-J*IA*pbSd(W%su-ox_wYX4OBX>8nAPQKE- z^JWTZ$5VK)KSlfFN&C4Jnm$S4`xz5^|MCvFL zPYX@l$~EbKC6negmN!#$|p1Tx{0%UOnkY(M4hQ7eP?Il(a$D4 zg*|6CX{`$r6CzAhZJ)x}8_DENNT#`aGA+v`Q}3>cw}(vxt}roqmPzwgOq3Gu?U5$J zh3{cC@kgqO1yxdbvnQDc1<4fiNoHw{WJWwQVLxsneU0=A`~6rGu|JwPCEN<(mnipQ z;xAo3g~$!bEXzvfr8E~dOxBw6Ca#<_QGcC@E>leUy-6<(^aDolh$-I(aC6{kK$Aj|AA^L1Z_>`RC+RvU6c7J z?9EEaJbEJC`{m1Q6AvvWrUt9OkBN$&CT9O2O|hzzunRXOV>KnS@%v=0(I`EMx1E!SES1FlEAnYWB6sE2a{2Y0uw&~e?~;kEe4Rj9 z)oSd+1m3hUasA&UN+l=Jxor}+l<$+Ps_*7RTK<+uL|h`JR3~HYM2>wG2M2>V; zF7oZdFA2~hfw;5rwEHcduY%+C9cMg#pW;Y68b|uvIQj_dFvMw();P{6mq__A_pHWS zUV=VDC9wQtJRir!^LcPQPlRn;G@iL<;^?_7j?VdUd>Ew1-Q#%ko$4SDy#G$%d};!v z8Yj@}Ogtgu6HX9Fl;cmh!zA zPwn~f+Pf;ABR=u`*&-fixpi(|&FIDVWI$I`?&indSW>ZJs&OOwDi-U-yHoItnp z@myXUPt*K(zE;i~rTe?;@jQMVN6x7@PA`umx-gD!yCiBI@B~~76L|G~0>?`x@c2YL zpBBdJJDPY-4~eH@_jrB2kLTLQINDx`WB%4SUQLaoTen1RTuh)KKY^7U6Uce5J_l5v zIq~$)7FN9Tg$+>7OFzfa`$ins_QVnXN1Q&B#S>96o*GZ$v>s!e-gn~YuG$TG5KF^V zv1DY#^1eqb6p3Z^zuHA^Vhn{tVp#rd3~k%TQ>SV?OCH2AeRUi&BjQ+7J&s4mV)<)Q zER%-DvZJoz+>N2`nixvu#Ncp=;Y!IEZnlocw|qR8t_m|Vj{kh)xLQ1pQ-8;jSP)BH z*I3Ovi`Dx>4C`jc&^tbcUTtIO`!pJ7y?Ca}1N$-Yj*nxvdmO)filxZ9SO%uV65LU^ zcjDY1gL!HU!y;nX);LDLQ%3V|m3R(IZ?C^~jf{-beKc{j5!R?&Gh<_Mw2tMGuw{?N z@OEAd-Ayt4&_0IKkD@tQDxL-B<1|ND<2^f$RW5Pb3p$nst7BPeie-G)Smu_ACF({D zi?_sZa&!!(2gLBaTnwXiecd>&>v3Tm6;k9?S9$zAmOX1?8J8VPn1{5>tE>+(?7I-7 zJyTRH@t1)hA5l7>Dv78h($q~z`!LeNJ5X<$iV!8M>hWe*t z*f~Fj>oGBSRgP!hNsY7L<46dKV`Z&47GH@aV0kQ|@_E~^SbqOLmRpTu8D1$Cj~6jo zA1DTw-(xtW_mgV6K6>feO7@ClTRB~4Ct~R{Ha-7jL|wkF%-KO&8m&jRI)`=xo@=AYKew(Q9NB0rF&JP^u7~? z`9X}<*pA`JiWr_cW2op8!_vwzBwmOnesMG-r~ND6+Rl;rn|OyJDkw-yw?T zs@pHSW2mw)hG%&(tacZ7d5zU$(G-*3F$1D$TRxiOC!&j-qK&6vKK)aYQ~fS`foRTMR3WG5UW! zhUC}L9N8AlqRT zzLES_CX&A{MbKnx1lQ(95L6g}_k<{>TBE3-@%dRbAKo&G#$QBn=5!?YmPJxFHIi{H zBYFQcLf-*JaBfiqH-3)bL|g>xevKj{FN#JQXCwTg_@{XkQ$9qp^hhLM&5ES)@JQ{~ z6iLx@5o9lmVCv`yDkViQz%POZW5ug{b0VTR?it1TW~%+CNG2VOWRr3&Wr$=^sYtD_ z5kc80(rS)iNKl0K!;GNT=qUb5SB}G?Fc8I!Mp1Z)x2b$OHYSpmK9OuI5vjQx!cB}| znkho-$VG6fQ-tQmL~(ya6cdA@F!ofOZ*_gXh~&l2NX<1C_kc($R*q!di3m!}iC~x| zg8v3b@UUkDi2Ni~`f!O7$Z z8W|$^TH|Apu8r=h&nMMqOt~ncE=O{IO(Zpcio`28QftpgQv9=UXCnA^Z3MfeePntB zJ2kdjWkkuhD1K9Y4i}T|Q;`h%Lta}W`E5|7?v;zAUHM4u*ASt3BoX@GA%Y_FBbZ$x zg6*Hed85|rRXE*mgwyzNI4;}5^;s>P{H$N2-m;>RQ+!)U2+2PvLIGk^rhBM$n7|nKsY47APCXEWiY6+uM%?J`I zM&MUg9=;F9A#AyS!}(!tIM06%XK!%0*6#}^?`#EpZqC4FpQ_&!?@Qf4DS{Z9BULoC+U9tCY-g(H|AhC z^_GV7EIXW_zTxCb^BVb7rZ9|SgTlz_7RK!6VVrBH@gaQB*Aa{tw)n+xw(bgN$h>gI zm{lKF)vZ!E{wKp&GF3Wb!&vJY#TC%l1%~WrwmfFO-$GP~KWXDH#`v%g|8$ZXT*N3PRcVHiX}{h7dm{ zgrpH6)Cdh>v9K>3p>(%~65tHw$DcwOlpji(u-k(}wN_FnYa4{p?SVL!hOi|sgr8zU z=rlNlbFLwH7lz{cqco2Zeq1QQ((y*v;~}B^B;IAULwR;3gvQgwl@vloV2JijR%;)^ zN%55)8>+n{m7m%#dj3J!-orzw>8|59(tR#O_uqzSjoc8e4-rD`wjqqG9inxnLTNH7 zl#WwFSu;&~e^%e5P*zBD)&8OUDQs0?D@_dHOh^b*drEV?5GH*YLh&h~?3pbe)t<}@ zrI)Z5#k*>-a_ARIVx3SfTn%B`EaBrqXx2M~&vin`DIKEy>_aJ77|Qg;p&Xkdf92im zh*0<;l$(9TUoVu~@@ncD`6RDK`-c$IOuEGzHY=3>mWI-BWhiyUovT`nSFWXelt-^n z&Nq~Qs!`K_LYO@zgw_eFleik?R0uz+zCqnX2!Chf_Cq6et{Js2n^E_d7%{ytY7Yw|vrZZLW}A^M^Ne`g zjg%d1B&(y5QuU2ou3_ZL8zWU88F_Zg$R5S`@0F32&yB3OZ6xQok*AxD{4&?b>P#bx zJd6~!FcMPF$nrWyGR0BkzESJ;8@VBz>64L}I$pnPBv{y48;nevZq)BpM$O4JvY~;I zCEprJ6*f|wTgCUsQzL_g`&48&&euk*1#jf8G=II`sP9jV)C`xl_D0IpGU~T=>8fJn zg0z15#E3=OKb0JgPpRRA2s>ChDhqp9ypzWp88%E_v{ruAjQEu|l3mbUhq}xxP zIVue2K*iw{zLyt@nJUdkHyUX=Q63F9a;=S#U#l9aRMN=ex5KbXTV?h2l-7k`4ad9c zaF!^}1$jQ{h>`AFjO>_d#5dN+fbK?KNb{UBM%>;Ef51qqZAM0@MoTU7&{rD8Jy`j^{XC4W!lr+gf0c*R{+r=^knS2UjeIyK-TxT5 zDeRslMh^UJ)EaI^%2xf46Ff)RJdFLBUSYWW$l?~QCJHeWh5X=|f1+%JQFnwzW zlU_cUm5+up@9a=2pBqYgQZPQT!J5|=%tAvjkAj2szhW?7MF+E6&ntHg=4G>B5`+yZ z7EIg!hI0SpP@ETsvQWIsBZ8SIj;i9E6dtTK`GQ%hck4;_Db_^afXX;RAMkLifQ>l`3RdQUY}~g9DFvE24{xiaeOF~E)C_> z&|ta>8#YY*;w&v(f8`Oad_(<%b?-|suY{dZMg5)(CGFHu4jvuK4CQ=8TDOM=>z*_D zBCUbuU|y#Nb4qd2yn_ku7L1`~u-3*3=Hydh#oJ!Isfux5IlUI|FUmPum?1gRreoyr zVCs1U^L3A4%C`%qkMfl zYUs6+iY0#I^Pw!)`KPMQN^!Nc25VolVBJS34ZNltuKh&Q$e_G4#G4uh)t7&SUE3>tjK5IT%FYjv%c=7esf(Dt9)BJ;#Ia-WbGp^MiOhCx{(?1Tl9(5MNFUVve3C zA5okWLDW(I7dwL}u_uU2I#=vs5GNJ$;D#XWEf~Z`wc|^JIG{MYgyZ<6}GW-|F}!s!k%3pM006hAbeZpZI*A_mB;+uLHH@(d$(1a zn?Y0*{x3aWs$BjS_ODGrc&YuiElB&FOUt?-E=$KZinUJ9L*#R(yFps}KZsp7f_NzW zEag8@nzwBYqS4<$`o1@aXR6?3!*QgyxK!ZSnS_QIKe36Q|ORsx>RVho zs`d}$e4ju(djx82KE?9aYr%nZRt%eBeWRA-9Z2tCfeew}3F(0(_f$U8S6uN2xChcv z_^xW_gl#B}uhqZ3;+_9KkY?(WIXsZWc;zT;Cu!}c+#`nuYM;hH{uSW|`i#tcP62~QR4pa`+>3%sTE+WExIe^qO3nI5 zfc9eupx?Uy0*epCi9Q{mHAWQgU;t6~lwX;F6t*15u89GRm7b-42C!sZ0Lzuj!PNomI~%|u z`PAxT0PDni`QHGFUk;#;p5LlHQ1>DaWWjXBTN}V2^G{oVj9;=Z~yfMqwuCCxRT2k7^c0FKG8 z0eU>O(m)!v9>}`>1Gyt_&I{u_7r^+#0aR38UG%)y|7t31#uJ^p6oB7#)mIw!R2fM3 zHUl-6d?1aMXbcE5?OFg&_Izm|cyoeXsJHjtfZ;QC4fKQ2)lVxUze154fvVBos}%=jthDinAXi8|E0lVpxtT% zGd37FA#8QCfhIavf3$(23k}+1#6Xxh2P%i{`sS-eKY7_l`N^jt1qOX~H)#Hgfdu6} z#cp7xa6isB(0`|a$^RL6D}U~Z&sF_z`Wom)Qoo{iVFb>oZ+GOfyhcaemxu;HR6ajj$uG`*Gu_ zABB7T_*2dOgdeGQ{OI-3kJI)2^?RN_i=zGY8jr<)zEj?<{E1TB;cay8ke}A&@T0gi{3R;;@hy35;X%JcSpKgI}GrG!6AzxJn_`plM3jsky*j_{{zuwp6ynTqi) z%AczF{%rookBJxL-7`PBsV?g)`s3HgpK;#)*u~df*m9}qFHS zKHO{KtN*2ZX))TD(|Nx97wF62Homkf?n~%tABL^<(eHLXu za!&Cswe+R*x4!&3$%k6Ae3&WC|Hz|p;@tVxN9&#W^4Qy#;VHgUl*j#Id|9ko?NYqM z!cS1G2aWP!v+%`*ZKYbL%m3esNNWRM_7CvYJbC$>=}S)R|7k8Jug}T*m{4C*pL=oa zy%*Jz0)_2ujT5H0K z9_PK7b;OGXN4>~D?uAj!>#7&!zxL*@7;i4j@aCU+-UN>JW`CTw=9qZXUKra+FHVZ% zfH*7aakmRzm@a#9S9xT%^rocQTYG?e6DiH-3%&U}!kc?tyy<<|i`~-r^WR=v-R-6K zJTK;4^dkL+7b`!AzneERbG$Vt!<%JOya~5?b9b0G{%yU<+AfXjy=b|?i*Jx&d^f?HHCf(V9^_4<=H6UfDXojW*sJ4Od3-^dt1FLN*SrWW zQmmE#vb1M zk?GBnpT%$W)}GPcJXL*qH}YoSC@&iSta?uIqUI*?$>)VE47Gz4^(-o4U=sIhWzZs4OqS$9a*xM*N4oC?m~%A9_)*jyHFLyg8odO-Fg;rMkWk z^rl~5Z+zv|x)Pqaee0g4}5NU@bRz*SN`%~$W{-2ee1!k0iIfu(36D4 zo;*;qPV?l~j~;BS=&AKfJ(*tClMd6X&1XEgzr%wsk9u(PD^GpD3{srJ#~!#S?uri{Jbdp#X+1vp$U|!zd(dHz2ccJVu7xM@>7H75!jtIvo?73) zlMXgdI$ieQC%taF=0UkT9t10v_dO4N*W^LaJ`Wn|@rSOSe4XdXyqTU95$6PPHcpjh z<+b%6as2DSjT6f8ss|4gtNIaf>inJO9-MXY+o^+h-Nt+3tyv_3DKZ7UBmwRwxrgEMojemOZ zOgftjW8UJS-&{TTRLYZ!{_@4@$@?*$j4Sk{QGzFP`g)Q%-GjrUJSa8VgT?bbc)!_$ zFQnyL)n}AEHdOMYLV)}c*Gb`{$*Jv0Y_bPEBE*^G!5DEjU+O{qwen!S2cG9V2&f@{ zeLeXQ;mI%Z=ce?}>!=!QOjs_u5%9rHb7kDQ|CKv?zH}$_lN-S|-Siux8?6?&QFDhI zi*$a|Aa@Sux%2ZlcTWE1j(wjSO)t4={be_N6~q47jkj;z7?A+<5?}FTDe8r73|A>E{oA%OkBT@PzI=GW9?EN%% z?UUxtvjlfq%yMJqST}B|O;o>b!jIYSMvo0{9GT*V*J3vc@4GRmy*q=4xU)IVoqobS zlz!VZH&)o)_{HMJ_+Q+(xy+62!tI>v#y&mw7k2kOH|{rar=zDkzCrH1RqLb2l_t5d zHPemhG0I8!t5c<0T1!aBaEBYCm2d2IH>Or|r*B7hO80fAr;9t^D2GF1+_;wLM(Lq$ zT#lEYKPk^~Zu$+xx8Vyo?eC17u8*aI7T6e*X z{RwWw#JN#NI=26*x*yl`8*Xe?jPP4-{CdHSl`*cowY%as!hx>8cFEm-Et_#3V~EYY9nn*E7r+@F>|xKhd6l^FwFxnOdon)*)@U!imyGP_b` zsQT-C<3+Bz*Tt2qFI;I*x<7BL^w*UJk=B|9%-<3AXdxG@5 zpW~|k#a!`Mj0)m-M z=`5YSdbskZ9ycth=S5ucsxIxyXR(L22s>Zg7aIs) z!j;D#`*G?^S3WfquU_jdKcDq=#Y24C9=nkA(na$ETsTu6R@H~&-C>RX75vKw=68g= zTHx9g^sh1%bc=;~G+N&jiWP&G!d8Fi!uBUFoYeE}^`LYYxZ4g2s=}@>puUdhx+?Zy zxRL~^`LLj|wi_>n=6|`6bJ+#6V*FlCxAqIatvdYl+J$oWTsS9PQ95_A7i=~N7Y0K- z(S5^k_^AtxPq`3t$%R4hU1(Yv{G?&&br-Ikb>ZU!7mk&c9&!C5eqD}OgkSufa`@mv ziDNF>7ghSD{U_n3J$GTU;ypX$g3nbKeHZ4!-HO__OP(}rFTNOf+E=%je0HI}`V2Ve z!me8`{QAO$?*F;qyI-;Px^Vck3#ae9@bIGxpGi#CpHe;*)bHGG7v@W2 z>u2&uIbUcAZ57L;*wuEq;D1DMl>76)UD&Z&brXKvb{GCs zKKGRWg!77{npBqOO&h`n)#7WNzbkylb1u}ppt$1iFCEjjsP>9k_kassj!KK-7Ru8B zulum{T_5}&_tBihJ`7slhtM^Bcz>u5Z=Uv{bJ4z>|I~-WrTcQYOO-XPBOmpl=@)%z(7Z2RzJ1vi(wC|c zeQBQ2m*EM0N$A~|7ypl>vyN}-djB{Z8*W3!@G)e-*pT64BUb=H|pd%BYnfbx8JChH;lw> z7!`NdsFELW@O#GhKc%QCY1%Z-Or?| z@M>IU(q&*%>Y4OAxTfs_<`DA!-KfL-JrCFh=<#+XlhTma?lvY}lu2G=P4b^(qQ9|8 zC0d#E7JVl{<8*jj#EIBcfSsHPr94RjmzTVJCVQduDLtU=d@81z?;L3@UC%{0ig z#-Nq_KDj(L*V(96+@C(ws2LGPHJoJBp(RF@XS@w}4C*(FJwGxAUJ@X zF&~>fVARgZM!o#OsPH02jeBHJ)q4g#h5tj(4Qg8s8*gS*a-dOlyBSqF->7%NMlG3c z)YdIV_CkSyhhxo|H|paqg{YE*ENk+W%|jsQD&7{AQ~b^tUZ zyML_6oy+KJ)D(31wTbaNW8=UDq6-UnYxV%gn@%d#-k>iA8tCB%UkeQ?5^K=0ItJbO z)Ibk=gEp5nXliAH=C?NJvz`VGIBd`&=$iMfK`;3GDSEhz9?t%OjK&z~Z-{;UXrNCe z{Bm{hF{m_l)@c)XDjPK?&8Uvv25p;z?$FJ^Z?Vr$jXL{_K{0s-O-}~4n?duqX672? zTw>6V*kvlRPQ%C7VE3gN%d@djUtnXk(P7z>*dI2WFpB$z^S_bkdM;wk6!Q-iyRM>g(v*_?Q3X54tE%MC=B7&$KYA%TS~2tC-W~8uiy;qYh@XE?@&w z@tYRd&vj@?MV7u@jGFAm)_~vmF>?pDvjLiCPry$`7FNRS934nF$@SMjd7B z*9VxNzQ7LB3_9|H`C%G-0aGW0HG;W+#kVG%t&; zfDL3}8|5xBA4HjSyD#%>H`Y4Vth4P||Cnn-t{AmxqfviuhWDpN^|6|CP6l0E$=vB+ z-U7$?sr;wXIbZ2 znV(;Ii);moq1?`_C zvDUHX4~AYMo8;j{56n|_kYT4ESi^x`alxeU>n7RGbKe1z;?|h76S;NoVp2=+dABxc zCD#)V=z!Ml=-^tUNrM*fJaN!OY`Td$8zyQ(nsjp{Hj-*m%>dR+o`=TQHpu~v-_=F_ zv)~oI@8Ox?JomY*9-7DV=I%u4#t5nWB-(t96E*XtIp0a`c+QMo<{^qnYs3a4HDQg4 zuV>OeaFyuG^BT{2t8=9Nz~#^7nkxNGT9k=r-|PQ9lLEJYkca+3*Vo@L|3H87njp^4FS&mS&%pycGi8mV2|=nRs9FvQ`*3H6RBxM=^Z!=c@{6a!$aM9H>mW0 z_nJbU{kwQ5ejqa7nZ5#g*vUK4Y~DjoLjOeG`Ks|e-*JnF?kx7uhegN>y1%{2dkA!w z=NW(aK@Uyb&-)PXPqgXL{IX{K-PJ7nJhQHxFl*;4v+nV3nfb)5lH1LSEi~)#We@$r z`%nNlCjsyAxrkYy9L*PI?eA_@=v=dk9yeDtJKF0)n@#ZLe6P%iHe z9eF?TYREeiy6M!wEblb4eCC*ydDyIv?wD2Uu344#n>mv-tBapm7n<^Z$$Qq#a~|r; zJC#>^sq7wUeG9X`bDPy@7P$6;^P*Yp&Y87styvQb%-ZT@Ry^;N-FUxTh|I^__E50^ z>DoD|ZAY`VWSX^os#&i$n|0$5v^hK<-qf(543X~ zK{t!@%{)Vxb$vGXWBX$gq|m|G#Tn^H9qtA8buhfy&AJ%@4s>!DI=@Bdk;s2pmRbHA zvHkhTi!plagWh6{+up3!R2Eer;%nYfrcC7&AL3; ztPRLEo4+S5MmLukr-Y~avVSp&J)c6ir|i=_6~&%R+$v9bL02>Ml`+Pw>c9-`Vpezf zE4>Dqu!TkCJ$1YZ_p_h$ER}sQ_G9KPWxrz{dqcp648UfA{TLn|!_#48yX051KD;q2 ztPXoO?b!EWKV?8Vdt+lfb#=CTg%`~_RoRnC)l)Uu zXIc>JN$(9$^%=!}&J<7T^Lz3PVOGFTW)0x}uaNsD=pDD#teWr8C;LaILp@chD|)HG~!hFLj7&B|qry=TpO$bL>oyQf-adFq?~>?aLp4{j{` zQ_x!bCr@nxwrv8k#opr3&FE{F0czdi5X&FZoa4Lad`p19@=a5 z0q-U=HRa8EcGIjr6+IP-Y|24rpE;fyx|}^a_9h!HVviBHTj-`fa^A(b^WgQ!@9_E* zSyk}V_Z`{aVs9>YqNi3bVSj9er>3&UmjwPY;QN{DUuaJSHsP5Uaf)8*z1mAtk9g_& z4KL+A@X{czJNLYF>b4hWCSJ0f_EOqGFU{TMrRJNx)P9YZCiDH~L@zy_>!l|9yz~S2 zO$BE46EC%V>BV!GmnJdJ^ebM91%A(AFLl`KrDof_RDQCTW(@Gs%Gq9u+3%&lfc^fl zm%e@LrN+g*)uA-|=Ec0VU!yFqL(@@M!pxkbP&1txAazda6DuWeApz;0OojW z(+u{$86&nAXCQWOee%7xiq!Ga;aD#%orUaAp(pTmY2htPl(#nY=bVDQ`v1-tZnNJ% zcPe}4(6OAobPsRNGuXdw9p@$IR4?sCcccIHQUzqYoIUuv1>PDlg);=;YAy2CAK5g&ABB(h(WUV|a&t}Q_iqY))QPjA2?ifk zFXy9|zjxNhcYX9>J!e;Ae6*{N553WSbYhf`s&EcZ~POe3bYfTu<<3`KSl5ArpPnbEc0DaUK@@6X%QIUB|d@(|oi(%tz_XpyN$v zdSv=2Gu}t3POc;$b>zXU3HPs<<)a(Ge#`efV7oBpSG|0c6vx>ZbayDr8Jp2Z{ab>s zJ?DHj&bc@P+cnHbN5TDqYY}`0z<&o||2tD_oXq*1A7_y@ee~*WXPx-aSraS!s7@=+ z8ACV|gtounuR=c`wSv~m1wQo7@zGuIHtr4$ft+PF@S)anXGI<9tY7bTR(t4}{hg1Z z8P_wC|G~?FEZ9Mk_EEcjo!P_cth-w~E9r7)>HzpC8#z92 z!C5YRUGzbALFmi@UdH=4+(+}wK6+i>hZx$<3gsLsWi)3yYdWjwiO%x64!+BI+b7%dS&{>rSaDFzsvvM|d)?vo*hmTJ__Mrxb zkA7+5qeqOf2)JFyHm8n{jKnQ0I^(CF!~(Q9!nye}KZOtT)0S*MMPzbrp2OLDzMs}- z`RPE6pGKSf^mjEsy?)@U3fuiO?RP)5CLUqSHqQ0u^L><`lKS}R73biOa)}#YocGC` z;fE8W&=L60{j}qbuRJ&(@4SvUhNXTwKaH`08#vHU0fods^zzehseZD@6Y~)2r&eY^ zy=V%q(ti5)GH1Y?uY2_;roru}?cMyy@!%62&G_Cjj(bD=cxQyK?}2SVj723s4f)$w zOMd4Z7#@7Q{p8sio|-crpCb%@n(X1HUw-&67Q#=-Rs9rN+)tbS>a9?MByHt#LfmfVSU-Q+JQ=Ana_En8NzQhjs>g5by?S12` zdKWm0-pBd!4qtt}n=|2KzSIQvRpb%=zs*-ymT`v9Ie9hC*T?_ltEiEjm*@J*$^WOW z;QV?rXTh9Fza0C&bM3CvIH#WItB0IXU+u-Ye=29l-F>yCo3BoB4(u6*j^_BP1?S-B z_}^ZByT!Tk`9#i^i3PaE_gi*fwf1LxZ_e1geASlEw(`~TcElrO5+lI*bvVB#{NSq# zoaOgx?<+Ik5Au1pm9IKB^VRjHoU4Baj^@7F`mL|Ntj{?%aTR+vS#O^ ziz*(sD1U=RXNXbRJBZ)ATQrV%jL%wGRGRpTAIe!&c$V0ayB1A+Xi-;i-}}R&$O9HF zT1TA9Tz(@iW-T}$g%X3&)}o_dSd{Rd_>KpD`sam3@1GKHa|OB%TGaCw;%LD2EilW7 zHK{|Kh_^puH7CyGGmG{-L-)jnv?f00NJ-*|o)fck!lK2?EP4!YvxxB+0B_xVE%dmz z=vYmQHhjRZh+(NpEX_=O?dvJnS|zL6Jh709kxil{li0h*!By z%ntE8yI1<@+i`wcj$a;6^3$=;tQrNM+3@EemZpDa;*Gwv$W46BX;H*YKb1P@r-{Vg44LSsy7=S!aQwcV zpKjD5&IvyZKsR^x00+JQ#kKs}fcU1@#6q3)(}nfKMa=+DKR?yRhj%b%JgdWaAMyP@ z_K1x97h05+Wzj(5U;HXqG!&eDk;xy(!+(x^YSlO2 zK)2JXkpr#NMzN~;46Eky{p$#;hBdOP%0Cump0el)_V*rLtmlf^V$pKoy70n#G}Eg3 zBZrY$Bfp~-`L4t_Mib|iZjqPGqRt(_jX!igY}NK-R$ba^RmKG3>Y}Y$-oUEhC-@a~ ze!0jZ7q;d`e{=k>Q*7qj@)lh~{{^qCN+-6h&T-=7ra)_$RnIFD_jtyl;)}7BfffaI zLH@oL?Qdk!=}#=`h#r1BgPtn-D~g!7Rm3gYkni1K;uA{|7q}lELN0?-@Y?`l&YD}) zhnT9k8_b`?TD`zNTN9u6Z&808CAKhmDsm07su;Ew^P5HG@TC&SWuw6V6@Kx?k60k$ z;fU3GGn%>jbAP>iY1JFz7r#N)yF0`8$5urE+Yb(k!y!zdewV4WG|oz4O5@V)3P9WV5Xs zF`D4~eT7voh#AdXYgP0lY^6K#fgP=CjlRk-9}UFzNB?b6k+N0=e8>M1&{1Eja^|Av z4Zssu7>K<0qUQ-e5QAFYs_Bo2_qvFWA^QgSTrGTdJbdlN{ytk^)g<(LkOS=pORO4$ zopqH}r5V52HDbi}T2vWZZibJCa9!Do-han#z+=NUrlF(B*h!svR;5BquP?1ybi<LoG=VjjDKj@M0Ne(Q_O zvzZT5E%*p@V9SOc{6>7{2y}d>g1-haPTYL(qvO6GEczNhn~u*6z&`F18+R!fxics2 z>cp5H7Aj*U24V$=1B5v+%8 zn*Fu;M}KVvcEUa4WRcsBQ0xlZ8oQgg#W5DO0k0Qx`}anRs-x%0%%7zzV&lNQZ03(H z{I&QZYteM8z6`c}+x+G2M|^QD zVvf%thrU*2HL)rUUkKR7v%z}iS@3>*6FdEc*lJ+6dBb~@zn&5&yq@^vbxr+QPnqX_ zVlBZ3r^4TJ{I&WCi>!YktACh#KDR2NwUs(R=qv`Bf%OA+U~_+sd5V23#DqIAZYUY%IywyYcB#H9B@4?)lh+z8^8yER5;FRbd#{IL=LpN@UqX^Kxlqc5Ki zprsvh95BGD@Re3IzXk0_u$Qf@XUtQ}xt;)91zp>@rfz|T-&iA0;9obbTE%=k798)v zS?`in)zIsKJG1KvGa|sc#I^6sVoqD-uVy#7_lp2E+d%C1VSf%H{PpKT=t%X~Qg|9y#9txEW&!vD zvDq#?{q^f6bos(xwVMWLMofUFGDg|I;rFS(3@7}lW$my0Xl$h=I2!n?By_w1?#6cJ z!cPL!P5~NJNM6ah0DZ>a^dUwc_({;2+QVO&y@8#`wH$f=2F^eI_02!^=uiam9UQ6{v%p~y9v z+?Va-=hXgz+$Hi_LMI0j6CbE`m%wWSXG(xhGro0TfT|B7uVzGmn#?0d=~#ebz_rvC zsE9G-#_S2y_NRf0t8UY>b~ZiE4p5tM0lGhjYf*sSE)Gz?)d4y{9@O=>@MjKWe>6}F z&ju>!Gn?{cQ%IUk*T>j+cP2k-1-U@TZriWW`Wx~&6d;dV0s6BR`8?2Bd>1)OWo_CJ zU{kvxHnrSn)4_{2_B;dBc25BH3j$P{T&rRKUC967`v66gZ`5j7psrsF)bKVo<@dHJ zdaF$tk8Eo7d5{KgCr@iWdg~pa73i-|IJrwn$Z1G`K3Wr?W8@}vBIj#8G#p1>x5)9y z>=LNU(31z=2@{c1A9R=n{rJd)X#pAuEeD?l=wx+tL7rA=a+NIU$aW|?njWama|78= z36K#S{|pFFg;4>jkG#4e-xfszIm1LwQP_NM@`@$}(vLAvU;i3N52QdnI#145Zh(I3 zOO6*~bcKdtPxOtgC&7F^cS{N+$Nv8HVysWrjnjE;zevKVn@f_@n!763eJls1N2uh z^4~lIscjah@z7Qroh~%m)GgMgmpL{y8D&%R88-HQZTclGK>pAhfSo@hzwA=wKrKR+ zFJ}kp`GY{>!fl%01H6-Ms)b(noUkeNx{W^RHl58wme89Iy*KU#r~qA1L{+tyLGQoh zg8A6kld#FT%ckSpA67C*>*@w^_7|j4*kgNe_BkD(oHxk02|n_l{>a7Z0iU4}(7eDV z<7Jz2%93kIj$;XO41Xk7aN8F2f-U*KCP%MYpsd)h3IpZ6FHl3V`2p$R+Gf*ca!Rj! z9i)BaH>Q(s`9~r7iTB_Q`Dbq?w`^RX(#R?Mo_w|y=rm=1pbV!2Rji~3eNzN-o|& z{BIj}-vt?$&JWZn`2VDnO*1FiG~)Hv__DskeB&^YZBl8C3pA71p%70p8Uhh z_-p+@t?d_xT?Xn|q)oTCgQr-KDzzp@*BZn-eUNg{&FfHfOFr!%lks2t)OLm3NAPYO z1#A(UK4jT65S*JD2B`))x$oTM!IGQ$ZWuYIgV9wwG%O?Uk-6ywAv_Q@5l0j-Ot zl2;4d*W|~x-w>n#;J3|(--f{&UnE#N_A@5D{EQsRk~=(~9N-1yx!xxi_N%|hS8foj zu*hKgZw9Mvda%5GgSBBJ_=g1PX(0K+k+pB1c9-oaY@9sH9s-R?Ygcg)7K78m+k7Dv7^xx+=?2kE6Dm@}JTU7i@MSyO{` zt6#7#FxKulHWkgqzOe1F*lUwr*b;N^;-1*TX>xAs1*>{&uwq9AD}7P0;uk~1=wOwD z{=3-h8RqkswSa(hY z>hFW(V%`bV!S8I!ngZ^s=!RV3h`0EtSFq~$4Av^{|7KROc1;gf!@ZGWS~A> z8>o;=f%*toFXk|3J@U8bVvEJmfnTu98No{IAFRi~gu&~$9>JQ&+E(beSCK0_6LnHCWBN25VRfWAGUp6|4oF`Hs#8K4)GifoxmZ#h~7qrmp!{=l$c&aQ%WwpFmY@cZmHL3-tF(;|Fk_5gf(k4>*@2B|zYfA30= z4mAqaRA6`5g4G>8%>q|UnP9bfN^belAU)U>s#{Y-^)e|`ty_f3OkD&AIs5DW4WVvE zs4As~>ieCc+EgNpXN@pD@Cnm+KJ%)F={9-zP0NK+bBcNf)H%35CPc$0h3Eq{0J?k# zQCv_cIVz!Q^fXlcYlo?2qcH8Q6sGT=PG^1mP>~%xbutA9K_=M=xQ0g~O zYvAhlq3S#|R0EEN5^EZ&>VYeyFZdA7SxVp_*GfRJW<4Fm6haZjC0NeJT0$cge#?@AEeY>*t2lWaty3n_ELP z;m;5q;@&CLcDQgTL^G)yF^+MYr(i$KN3|ve$%sxPeS*o$4OZ`_)V$~&qNd!}o0<)m zk?Gy>)NRNL(X~K+gRZL4LCWfm9f5z|?I8UX60E++F1&S!`V@p{*yIp;MN%^&Jw$CS zA<9M%bKeE))tTV`uS2i~ydm?*pMOElJic1y96r!4L<2I=T|RX-VnSqUgUm~XsN~vlaSwglp_R zYTulL-ka3ZI323dXF{oM7^*|{!gQ!>m`bb<)141tnnsP8imq^-O9_{WYb7%Io;oRk z)RXbX#)fhQjtf=RK5B@R3Dc)h;9nM|X7|JNS7pZkni?@&uIk|$SBe@jMZ-4&*|C1HAUE=CWT4V>2Tuf z7k&uUm|3CP4{XJ_FqNMZroP+4G>U5;G<-KYOy=%k8sZ%$L(?!N*ALU{VIgWyot4OX zp_)FP8ZJe`G$SlbuLgz5G%8Hva>I1g0WGb;T|SH+o}u)4fX^-XLj*NK7E*_$RH&@I znRD)i>UZw51&8USSD5x7yY~MwZy?LLt3&lK^T5om%pveN;%jPLc{=2Zb11HlLoH@F zbbuO4Z>gK~j+#bqsjJjvwnLf29OTkEJ9OcS zU1^8yYI(q}=4b3W|H!Vma@3Bh;!rfSlr~Zq%R)UYKk9n@;LxmE)SY^1*B8KKEW!^* z+tq4_UB~!5JKwGczu8rl|0O@LYiViZQQM(}nhq8Dgql`=+I4t^UCsL1bu-egtDbhv z>j-Y6UFIOWYG&fk_~Fs1c8&Sfu6^h1vixpWB*{(XfvJzb27P5$_fMelS-5WAV=j0U zuBAontoit18N2$ow`(SILuwB6^{{J|8DB3Coi0Ta-41ilM0`Dg`cB^AIzoM1ND#g1v8Ii;9E<> zRXok9QN5iSKGLbQaZZH|acWbtQ~sTtYFo;wiWjH_w$Y*c3$csE4h>pQEv^07Ahp3B z?sn?qV@~~e+$o=5ol2hTR7|>)x+zZ8et|9QpuX2khd#o_E?|ozs8!Zxw?hx8%hjq( zgqD?#(5Zi&`tXNSHx@fpr;Ag0b)4)SI5cHGb-e~sr>qb?40q@ib(Q)8^HKZwg zQ=_a0YrU1MsLwL)IL_z*UmT!7t9q`ul#r}j>C>Z;4B?zNr&UlXnGU|{7? zcqMAMy|XL52tI=Cnp!wi0w0(R52Ht5w}DP|X#}p94&6fjrSYSA;92wpm}7RG*=JYw zS-a-Guq);Z#=sxn!N(frf>`MD0cIz-p5hC8%Yf^IU2Epq6@uMwooLsV)poVVUMm-c z|K|7`K6iGsL;IN%3gM$&J%_G?D`Jyfg9h4_6=&BsPP-l^+ckWQT`zXn_2`*ho1rN^ z7GGmtiErlA;5JTbn83H)seOq~y+FU&QBJMmxARS$no!26;!l}#pz+0Fhr;&$uSQ== zzEh9;qSGNxH6H3zn?6ph?2e84JC%gYKPlsl}agUZ=j?K5G2|GX$GDG{vDwQyn_El=<_rlR70%Oa8qMbSlA72-BYQUck4F}f(>}ba%hrSwxZm`RJ6PWkFZMzPQ+nvh5hkC|16#<eT_`i9R}D)8BGMk7Ql}|LEP+?E~kCNls;gZ%hN|e&is3j$mhC?7fj? zrbAEM;Kvq=gEMW8LsNfq=mz)ApXt;@@Y%j{%6JP~TIJBFz792Fj(s13JebeFNOI^g z<6NEP&{AaF;)O%U8zN(5S`Ix_zvoaD;F>b0?+SEK9}XLAhu%9=BM(^fSibLKKF0o< z!q-!5c%;#(DesZtPWTy2Ex%C4{+>FB4On-W!@tLFYV>4%KsMjrVeJ54E$ECN1D*Aq z`r#&iy~Lp;_!{L&ZNw(j>Z{MZg6~vt0y73Wjxtx2bE*)%FCFOAsb$C&KUmG&^hZ2n zjdE(w8YlaiP6ZK4|zh170@x8%L7bI9n(TKsnxc=k9o8~Z#6oe#j*+!vm{V4g+(#y!-DL{_dv z|C^^qU#3oC9c*zg&ASx$YA{%RDD&_+*csishiwUD~3mz~rIaO$fbPHG7`)$nVljx*Ob2UjOQ zC;efd6GkWg5FdSsyQt}e@%~&i~rT1>D0jE|IIHEn%FUd^C;@X#ziR2O6^nV z^MJSb3)l+xyuR+#iJB243`c0~G;I42^GneP?Q9#NArY)g@ewK;5TQ5q80Rr-4f;=o zwtKUka$`fGC0M`GBGi8$Yu-Ojg?|&FVccICoP+$S*ZMWzpTp-irw)xr5775?xKn2A zr=V1UV3_G5ekB4P7I8JBca9-jM0s)7U`M2=$&4 zq0sgb`V@S(L#fN^7op5%5wd?8p)qG!^H(wlq0dD_z&*vOy4YZoQW4r38=(<s+0p*>$v*B1Sk2k(6BCK&x6XRKepM2^uB`Yxiak^1y{gqp75-jNaN!uXw#LBk;tngw4~ zj-cn45v=QxYHx~EujoiM&5M-xlt^`21|9n&^z&QvLv7oA{N810q+0Kx_U++FP2Wq+ z-0l3fF;YWzQ0w??ByqiwI^Q}`{{`}HM?GU|?dGkGRN5)VycnrdC#V^`lN!lu zBGq+OBzYc@GGC3->p9bcb?kFw<7To>iZs|b}wT!UmU6Q z71aLS7pW63sJGi9N*UQvx)>O#R>LFJWGi)f|ALPvk!tiHQUe$x8=UW!G2Ws`88$>} zD6|+VMCq!Pn$u&Vbp1EzzXPA}u(|~GuRo4b>LcnGLw7FYJpkW;Um|t+C}X{$Mlm>- zBuDAyyeP$Xj8X&dC`~p+(I-5L^NJ{qc@?R-e?;m%@_Go5-pAncDfE3ArL5uVI>d_-gtplTU-VmksKSt?3xRymD7wFnOIZ7LWJHJ0lr7lLv);d}X-bd;5 z(I~wJcli-f`XWC{-#}aQ+$dEZ9HkoYbYex6iZFf`XdHQidtXM82!*^EWB)Vgq`vvm zmel0-j#gvpfDhvH55}HcGn$^zQS!SMrFw^v@pfdrHA*FKMycPY(exgU);a2(ukT0w z^YPI-GcH=eeD9hVtv|)@)O!!2{`s;;QK|r5AFeSCsdpX{tvZ9F^~=xEDt0VdYtBUL z%AsgksR_P$M6@0OKhuNz8c@?5+7?sKe1GX^&IP!3QQMu`qkNs2?m-@GLQGeA%yogJu`Ali;k_R=>`&^IK z$o1eI8Ljjl(d1B2cBSriQ4FS z@YX$AL#U;`gU>xZqshmlU%=Nf^xJZ&%MllKGF=)Q;ZldXE=5vfy$7&eCR4AzFZI^B ze;HQ?zE>OrY;=qUwxG8Hun+dS^wB7nqT#zqEteVrJCFM9f6t^A{0MYC2;TYpl6veV zS47J%h+YA;>926drOLm!bhpr@seUdstm)DV>inNUSNB)*ITzZwE-iy@Xc>N#y8qMZ z!_bQ!0}lEe7-O`qF}(^Z#%KjHsPw0c_+o13Pooxnrc3eY;HxGs1);~pQZ79j8l(AH z+}|ZeC0#Mf45d$jIYw=o#Hera82t=<;wqPB_CaT%F6wf+c#(0GchdgyDc8g1Y4WWj9G^~Mqkg0(T21b-D-{eU%9jw*lWNxjCav9$R$toUoXm~Ays2Fk?U0XSj~GMqbrwV zWd9{bVc>4uGDfivUAnc}r5k-+Dok+cW2Z|~Vq6Nyg?~pZy&Ph7@CRUP#Ol_A7&(8Y zCr0-eRjnH%+Xa{Q%yv=#&ZQpUogIg~klXwTF0~#&zlM}p{p(AAh+46-T#q4tDn@%l zVzi`0j0}75jWI6uONDOW)^~TQ)*$NeFLSBPh*(YPO23GXvD)=%tcLH4(b9e~>fA0y zFQ2%GdvmGjfA{uqsY*BeCWjsXlU&-g)unUpSoY0h)u2kOw)}~Vz~-fXd~Ur7=tc989xyv%)pKqvIgGIyvM^So zf5m?9#H!NMIHg^W(?n$d<@z{}YyhWVaSBTTXyWra%tB)tf;y3iViHp^fgjfyiMgJRq3tb;8yS=Mg zHR`Iice?1>>@G@obW!zM^!s|6q&{cq>2@fIzUN6gvWvghBx&O~`lcl$>EgFZYWtdg zZX3JkpNua0=DRNH^DIdfcPHtW*-5H3JV^%&lT?~sQtgwH=(S7ludkD|{U!afwk2xq z&@L+JLr=AmU3B4ilCpsLGbc&ePGCKgboTosUHvLaL&_#;&m;O??V)GY_(YwFPSl-B zUDW7ulDe#bp1w({8wuVYla%^plA0AyQq9NUIGv~-8x!fPm8k6QiE3#`)R9VwH(Bqlmh^=l+jzac@Ua|!f>Nz$AK zNt#s|m`~^%_aafbR}Th6Pyr4JM=>(10oS=D&=@E4+Q9u0! z{pS;j%ShDY1Hf!e)P&iI8q$&6Kys9%B-)wfonmfoPB*3SuQG=e^2nF(svonA@l37S6;dIluw zT)#wp*Edm-g^Bd@Or$P5J=0nP`w{#dO3;+432M`mo>X!4@rp@MQjY|+?#BO-!%A18 zT1NtJhsS_Ko-^s!29JFo(f4aR{hY=p=ps0OOia+Yqy+8AqIa7Q{W2SN*Y$hJ+Bcsb zk*;JVHceJa#bi0EB1<;DnxB@O)N~tXOCq46Xj* z$*S8B{%?2FhuPi8C+?<3#k#4})~=d>Om=@s4^3pW8hKXa{v{)m^`0v+Gg*~Q$(sCz z-jGYXX;oM^)h$6!(Oq3NpPqw*-*uDY^JE3IO;)7{=mY0zbl@{ISu5kghuj{X=%#5s zyJ_~9-RNW4RqjdjNTi=<^Y`?eL??6GC#xQ^nVgMW`$7x)8i;<*6ro4va{7AOx~ap5 zu5v-^$)R0!7})IM^iPH6WxzJGF$Qw#hu+UxlJ!rmWL^KGo1P3sKJB{cZ)1e zx@uz%eJJOq$gny^9cQHIL{s)gN!@#ax*l z*CpxSeJNGFI;AS7E@Qk)QLW1<8gVK`zg02= zl_>}80qBTN)j&(CzG#=Kh-UO1?vSdYQGAY0RrH}$Eh$DX94m0rgiQ}yPrR86RmreHIDy;IVZF^GQOL(+7e?~9=Kk7ubm4G#|Fw&i>Z zd84Vy+LNk^FH;rNG)@=O-m!=CR({x}*nu-ifQv$Rbj-_g43_YA*()Ss?9rz(d4{xVx zMV&OwwbM^~Bt6YH0DF)=<-6$b4*t>sY1;IV9?pGI>CKd?HOo`<9eOWcJVmogr|GI8 zP2Z%ZY2(;5wV0nq+&R6%`_Z2rxkkT8Rm*Xys?Zoc?M=~w9`uU-Dn-q1^x*v`Rqvjr zs!}Or^KqJz@ACZwvfiIc|6z2D?EE6>Ee<{HE2Zn_B(5dt8gn{bZ;{{M=)o%_ja=X~ zjYbbg8l+K}4mo9}DRK`ye3h=i0rWq=kS@P3GnC?=p)0W&>N5rU<{-mK^j=3U7tl#H zXukJ5J=ANbQ=1@NU9P69TE`4^8kC`@8!}Y!Xoh_EWvJwz^oGBhM!y;QvENP8h)>e> zd7E@%!_rj(x@xt`kZE*=hMdb#VM({5>bkYLrduW7WvFfSbe(9DuKuQU^+`7bhL)#iC}>NDIu~UY^K`wH%dhUQh>8uVL+VtQmK`KJtRzMP>Mjok7}ag#IeR?HvZc;!~XJGc7Z z1kcZIt?A-ckBV+B0pG5;41HQYLvNol2K1-*bnDnsx4e(LRr`XIbQ!@dCc zdy}EB=VnkZAwzzz)8$^CuDG0ZtvH{qpDt&i zUB73f>(+vFjYNl2s~{uv_{=LqhtbWx(i!spJze{!rK>meeC|%C9x!wQ=iqXNz>6A zx?XV7t**c|f|eRJGIWCbhd}?|xyW)yhI(QH9V6T-KG&_m=h59qnd(wAQ~ereszt+0 z6+uqP$SE@Be|+59pP>-+xG>SpnSxs_Kh4z1A2YQtHdA}und+03sfS%NRVE-)^^-G{ zH8q2nRQ#ffo49PZGS<4~ERm`1p_w{5CX*TynR>n>Q`;GGgap)_$Fp)Yw2vg=##CR z7qW@p=&4U%WvkZGY=wN2t)^qL)c01VB0P~1{Fk)3brj!S@K=Ub7RlDIZrMuRkgfT^ zzCM&qug`4#{CT!^05|YMrUs@VuLszAKeslvaubh)Z#~b@bVIhD56V{U1=(sdA)ENo zY^}MMC8cG_@GMgk^E0)MVy3^Xfw!)1U2X4Xj~ze7?_#~PH7GWl9!c4%P&Hd$ZpzXa z?<@^Flc@_Cne@2LlrOp+Guy2@Ip`W+^y%hS#ay?Z*2>nyGTGX2E=vphXVE`COUt)p z>U3hJMxp<+$gsyb?!%TYu5+u!CbzoncT;DI`QdPuf~IFFJtRw|-(>1z^k=Z6S8Qir zg-pd3&y*+E(B~vx)XKHMlOm*^xpQJ3^Y?7r>*E7{*3Ua|7?ljHRi#nM~0=|R^ z`9@{xR!_zonJK?nnOeCtQ%>|1QZ`EiE@W!f9PUZRUd`B3+e|HC+-j+rY6u-0k7uem z_OZ2GmQH?=r5c}SDWQ0l3eRQg)6JRcF$p{Bl}XGGa>ZujXF&VeOht0v*nli8%E?lz z5m}l(G)tSav$P>Ni})Ap^{NrhdJbqr?B^sA6EQE}zX+`5t-t{%)RrX_K!V%2!C4e8nxuQ&{~xEt!+6 z1>fZA#E~3r9+0EDw{sNKI#*j(i+t$FQz@T3S zP5UfIZ~pA1j_Z2S`!7$I2XKGaJVn&Ylh@8%HH^;Hn{v7G2BzJ*9A!?)(T}+~+8dOk z7hmV-{<~f({G*p1_sP@0F?kBD37zY5b+}Wm4gk}5EaRu-sC@Sv-OS3-U|=gFr*8Hf zCAG`Z!OwG4%MP8uJq2G8-(32GEJx0`96jg$ybC$%gbjUFG*`V|=1{vZ zM-4m*wBt~|CS>HRydz(2hvsYcgM2kjDbU!v1)AYis6%;$@*7d8tNjY~W!FOG^IN_8 zh4h!qCtf~ZU2ErSP*6Va1NqA9P@wSb1={;{q532j%4{|T&Q=ch5Fd1P^mS*`Jg~4 zzvXGbXW;6Sujmi?x|Ua<->(nnN0~x3xl|x) zk$lBO${D+t%FMsz6^^3UtX`pazU*-^Stk1o(b;dk3Uz8)zUI60b*de-zh=%rXJP-x-dRUi zl_mc^1c%`6?(TF$BPmEhf&_=)xdh9_gT$@{Z`>W4MnfQkgt#Y2Sajcc&HPnzkO zH^0~OoB6%<*88hjtEhAKsc+e?v+v1~y#sw%>(@sX-0_i1jeKR*&-8s2KWVhhPd4T9 zmzDI>-A4X$Jd3Y59rKZ=i#Z1koXgHW@~n%GoFD5WleSYc=*u(6mpQ0?r3yLtb+@1N ze&8pb&-|q5Odnb6>LX_>U@HsbUAng{DCi?MS~Av7^N}O?(Wo5tI?GqGyz!NH-TkCJ zF*n`eCj;vE$g8YAa_&Co;5l!pd5LlJxwj;6Zno;?BZrpx$ie&g(U!iT4(5FFm4dDP ze$&U6;M3#v58qM zo4(?Wf4+|y?{9lcSuk1oU5P{2q?g_z1$?Ah3m;Kz*WKzPv+%uDEnm4m)>pdj@Rd55 zePlDV=Cijrzv6cVJwM}1Sem!E7xt0FW{2> zkIXJl{}f`3&(0TQ=&F_ndzE~ozN3#^_TV>ZzK{GA$?wD?AL&%umrr(lr4{FKG_`36 z@sl+(IUhXyq=qv!RF`uopP%f0%sHCqE6$5KXNEDJao&A_)YDg5*YuZ%Y2X+u8qN2U zrvv;Xp^=|Bf(w7@E4z;R$}{?=H)G+T7QRxKvFSr*U&&s?mv`d)Wi99OutR>b@JBE` z{3Nl0pJe{vD~-?jN}+wea&oS(`qedFO@R4wS>ssFQk=BL$Wgq8wqkVo-Ybxh_ zD?cfi$4~a$_T?Kx&aG9x(w=%(e#ckmp{^L)Hl$GN)Y}Tq|4V!Qq=3awvUc{9p{1$! zm*nvXb+pY_URiya1BCO0b1I$jY2rm6$r4ZgqkZJw3_lscx!$*>pX4nHz4n!t_}V?f zSFWt^mFAP^%TA2lm3?L07y60u*zJ&yEKTLNh;wUeZa=9BCNs6N?WC_vN%ocV!M@DN zL~ild8*J9<#Gc((3OuF0@!N%QeeFTUj7a47lA91;=Io_U;(S?;g#2#t6^|Ld@@BBF zY;Q>n<*DV|aC{#Otr_DheW2G<@oRyv4A|u>l~eKW1ho&}xs!ZN_vPM{uiSGYXH9&i zZ*Ba8-cchLYcZyD@D*2Ln}My*r+mfq#8|ix&a^hk;> zp|nZfbT-LPW|Pd{Ws)z)Ok%xbk{7?3r11-r_&hgBR<_68Gs&=XCb7hrWWj2av>9XK zyFzS?GfDoRuzl1di=LRISGq|`<}fp-nwdF?%yKoCS$5?#ON_l)3cNN+tt%$!5^j>y zBTVvYnMs!6+d%A1`)HC;;HEp6xS&}E7BEX7dggxuhisM^CfT&j zB>o3Yvh;>Y8a^}e{GIrqEYON~CdrAu68N$lyKSHuU`HVL)W;+d3&{bw=)BJ)#fWDs zKJ(oRNy-d0$rkixCBF^1e(3AsFIH%LH!yDgVj01;0oOL~m-tJUz5a6O zguf&{_Lsu;^g#iWv@dCr=<+5BNZ{OyfTH~59lx=I`Q0wa@Am4?elorczgPU$PZ{Me zPv-f{rybad@n`NDe`$V%YmqJ{3C0(*r-`{1O_DpxBn9u9WKDK*TGcFz9L@4>kXaV` zo0;p+EU^jjJ7&q0PT#L2hK(lH@FB<7sI4q!xm4XO#oC)CmmB@-X_mcavs74XmOS7N zJ~qqXJQlfM$->_*CMloUENNxU@&+4|hMOg)%`EGdnq}vDvsiyNOUN!W>kFA>$ThP_ zW{WH+ZDGA#cyIc8lv$R{HcLTdiv40{ojiQHZ`VmXVX;loV)d4g;$Y^UKz5w?q>ryj@JB6F@f$Ld*F^Uxxp zwJj1;*&=zcTQI}-tQOh($t=@e(030x$H>Dz+E#)e4?brZi=3%wk%joW7hS80S>yut z4iRhAb8?D&w=-t(JYbf*5oYmEGfR)pW@(5onbGk98<+E2B!E85i(lV=FpK4`S>jHa zWnPk54uqM-dpq^Af;{10L*j0V>@Z|kW+zT^d9Juco?<_Od^$F_h+SK9=WG$5UKVi} zZjnCz7S<}I55P_$cRtvwT$J47Z@pR;F*dPC{~s)}#gRCiEIh}z@ZFb1It)SIREu;Z z7q`i?0bDb1FDif~Usu4K>tJDxH2SCq`;bFVa?sl%_Xb&{^LUFm5$AGpK0FtFKyFsh zFKt>{BvWUL?CxighuFAkut+)R1>4gGTjbqPi`YCZl0t3&`qnJjb6DgoK71zr3-}id z?#Kv>JRe8?C()l1sR_1cff+gq|3_Qo3)n_qIj{0r#Gd|ag3X`t?<%;EDb%LfBCTx} zF*T@}Lg>1+D7MUK0--+0Zx5(%yi;N1Te}XJrzku0lk=f{fvId{w zrLa?EAATlSB=#tJ&(cp97$eSeuAHJDezC}=B#Tsv0C@ayHVrR%Qk9Y{zm$STy45$;X0qX z%3_tud90kDR_6G&%II8H*_PGH{4IE1bUQd$ z#hlYB(NGXE&BVt|<&dvwl@|2TZTf0HHPF!2Dis&lsC4X{a0D1Ew3 z^0V(f(vedH4g+j=7wMqL9Hr|i7Nn}%-9BpKiE$}w%=Tn<# zXPY$dViP~MBbwVJt)flJ=d?+$7giY#{)N3ww!Ws$eznSw2UhWYVwE%Rtg?;RtSoM0Ttg3SFTuUOYL(2ltpH7!T}HkZSf%EV^yNG&>#J}c)83stOeg;OzmUUA=y`6Hj&Yd=~?XXGVKx%B4 zP3p2AzS$;ER@lUeT5VP(K+J^#q|!&5^tf)5(aAPB7G#ss8*H*`IkmjlCW{u?q&T#P z+VA=!xal@Y@wUmq+5u7n+gCo=r0r#!+>NoxuRq)5mnAm&%(*bfid~aUzVln^Ys7cG$=Q?$cQI$Jk^YT^IX=oJYoRHgHYo%4{#2XTbB^2Zw{g#xoNgk1@^aLM zkNC6%*|$S&(sz(eJ`A+UEzbK?gH4{%ejLAh5lL=3@n}qn2SM)R_j=SV$CHekB-dA&M6*oq0hI{M{Kv<&REIzCGe5P0BO)SK-T^cAhXK_$Q`=? zzGb5Q1hK}`w_!Hv9c+^W^y6^+Sw~zCci3bU_->v7a=%xA{MaHuUY8A!fJ^~0^a1go zWPAl%hkEe^cMJKl*sO!?HsJh!<{ZW6>DU}xIY7?f^VerK*+A}=C)i}{9%9~1p2_7nX{-~Gbzj_50a-UVQ0(?`YWm$J0w#YQHm0Cv(-IM1n{RpI1>`dzfyCTFRE zlK9nrCgUU6*2Gqf7)RlAabM2sX^a(Kj2RbfV)zByv0O{+CbPg6n}=QOHNf8* z$R!YOZX+@yZIXu^JChsJ9c-UuoWSkFVK+A|7|7K(BHZk9%pD!YxG>H84W&9(L zZk_R+bMEvxbS9B|upcMPJLXmNugBg zg}O@E!uUwO5{Ng1{PYJupIon}7Uy`NXCUff`?_%psuQ+zcMve$tLfs*ra+b#*s!g zNd(hVZ8Di$C6{1KMJ@}vX4Agj(I$4nZ!2{_p(Xmhr>zC&Vk?_)l_YK*`CS0lwJX2f zv^`;e0D1SOc0ao@zR(Yy$;X>6j4>|MBgdygp3dZ&{m1ZMu)W2=xip$u9LISyk#m`I zeIqqcb~ybt$R=~qGakLWv9p%kUhBa4fc>xdGM9cRJ&SsyKR3b$P3PP(aSri2uwpFz zM8EjbFE4xX+kzjPowzf zKQa71f=}PWxU`G0ZwGaTj#T{Vv6eAm3BSeou-KRJix?^mWxNExnV6$1b6v*YO;f8` zrC$@PtZ2uzVi&7;yYe?xN2^#`aQ#?=x+!jzat>ChosDb3>{dCHk9y>9fgWHt)Z#j? zrB#Y{;@Yqm*UWvm4hFxPULD!kD)XvZ!OL#=#wY?XuPsDiFSE?kp4 zk`wH2Y0mY2h8?hl$%TphIDvZ)uD%D?^`kfkM{}(^+$wViaV<{#C9wagJ=eNztdbv0 zX7KyKtiiW-L%7xk<1)c2=e>w?GJktyc= zbIm{1Djt4Tt_7{~#A1~N{0;DGnpI|SY$iTL<5!7pT-TGA@b9g10^Oa#Ebz6870j#v ztGu0wue14^Vip)1?LPe706ugW{oI$o|LDh@t*x>f%mHlufQ>cSnhSOr*qdNyKxxzY zyWJn(h~w@^tGJQB?!W`?8mzfe#|@SCvTto$+E9JXW^M* z6xdBq{Nw~ODY1SsF3^v6NqN?Y?)%6dIm)w4p64WUKR1$RmhWEs$!(s&=FZAJTy%VT z>L)pP{?eW2MVWba^Apbq+h6dL9{4&9efHc7etLp!o<|LOk1cFE=J1zG_S{>3<0n6( z^U`rYN#mL5dSsWQBQri^3G$OL?kD#>LhN^WMoJE{WcL?C9)J0cXPNJL&Qz1wzTWVY zm-yCZBl=hHJbo#9*5gkw&tb_`<(oW{e&;9e!Jgxpar465+vfRkH~g4|eUJNo{AP2X zn&)Jxvw6n7+)rBYtkOB&Pk!JzagmpP@`hO7gFDM}wewIq+pl=`T#|d+?`=FQ4)9|x zBtOZv%ug2l>?iy2I})FYJmPsfIeMGlUsB2G)C&I6zns5J;@SQlo|o4LlX)J`jCmM8=G&@clmoSY1i3H-t_a5=0kX&20GZEcbRzqriF`_ zyl>|vm)d$s1IQv?^0d8|xbv=4N2py(FPY)&C4G4x=~W-zn}YUq=RKZIUNQx_YpuPc z>@hM4?0 z9t&C6_HZO8v?r0jD)7SSdW)^q#F^5KT$8iD&MbN7)YUH~z{x3S;U51UiH_ zq|f7Bs}<;9=Ow#0cuBAIUea$hIax}d%=eOfv%I9+Sx*^!%TxRwd5YT;PuUJ{d)rfH zT=10Izj(^oXio{<pkW4LQk0<;3=+8J()}1Q*3#Rl3vs(UL}oUU)(4!3mD~C zcB54JQ{J5LWIk?BN!sfvmd&2x^1)Mzsd0)&Z$7}Kq_IE~U(+-;*j55O6 zC{a#E?r9t4NfV<4bNsuqM#-ArD2Fo{CHWQcpmRhOBfpDA$)d>iHj2j}XsA)H4>3x; zfkttsZCz)h%w?vSd(Dhep_Wm;#~(v^qqJyfl<|&6x!T<*p$4OL8Do@=lZ<>rVU*gF zjm#BnKa9VFv_o;j4~S=fkvb3nralW7$yG1{i*EEw+N@4qH7bEjv8D;t?qZG$x(*UEaooAGHONf6ZHdYZURDkV@%Z*Z=_GRde zf9NhXAGynDC@GDA+U-DSxOciH!wyG%e&^&5O|cFtV}|KiSb0%St@4ldALM*ZwAr*av@ zE{8#0WH*Qv+M3NE)3O-k`%HWj_}N`*y>*w4=qY~JU2a`+m-Z*!CFLMKB*3c}B)+0S zf}nd9_-+x(!uNUQ`OeXv?+@D*G{|i9kGD6-k7;}>hfk@*`Q)a%lvUc=85m3W2GG$U zts%cQ2Jz>+K#vv%+0}$^3+owVUNwWvL1!S}3GU2m5KsII7;BJoeCt?YltC(wFi3Iu z9r(Lp1~IrBFD-0q_3{q{8K^)+@;Z+y# zjpjT9f1eq|Ex;hz{0$N}8Qd6ygnAg{!T^H|J%rCm1}PB7HS2QUkqNd%DT9?#z+W)lD*YgxWB#O=ITxsK%Te#hGg+2Xn75 z|4aO@%<*)spIkiIk9ql-k0g$HjttB(;?6ulgPGT(AM>SjbK^IX`A=FfhgKaov6f># zlKjj;WbY;~m~$unQ9s#!qn~7E?vxhHQIs%=__)d6{1I6Q5bkAu@ww0nD|*ydZAgZqj)Ib7+jhe|-5&JXZWz zjUP$%+$6Lz^Rbj<-l~Gk3$vDaiB`MG+ZD`nveZqwE^w2a=<8u|lZdHqa&rvw#8rxT z5}0?VSUWd~{hm2h8oJ3<=JY7L+f71tGH=#4=2F=LZiAb6Ft^Ar3$cZs)h0JtKx`L0 z!I7J}==tdCCYsFLNQw9mM;y`26BFqswov9lLEqTz=-uQd->qUE9rPSQPd?(ZOkuv7 zvESw`*>}cG+MaS_PHs2Zbd)(_4!Mb>U=w{mM3AGsZW6SExn|H4g`T#HnJZ-u^VvLb zlLq(LzvCviZqYY4n9JlE^E_Q5#o)-8<&OhTr+feDzOVCC4k~?tbek?`ZqXe6H7__iQhvtpRhj zE`Pz?zsT)=;wo*Suh28*F22lsMFxHe)OKJw7l!e zI}xr@2tMsTxGS#woq_J#t}>SWj}OoXc|$dkpMh-4TdrcqT-_b7G5_^dS2+*$26OK$ z^GjcJWi4n|zNux-bL0}B;*cNmm637e*!xSa5`tWp3$9WLs=;>6Qygbr>ZNS^;DZ-( zAE7b*t!MgJ+uPcYmN;;w+Hewz)ghCvpo>(ey|5Qeh#^nU`K-80xk_Y;KVvQtgG_l zP}aa2%^E=CS^LL}^<<{9mX0^;0r_;2Ev$d?+OLy*_U|P1Ev%0d&`A!?>css9*4`-A zN#;~$9jm&m+taj@q_^lKzqMv99Y@ykYS&3VqJMeEPSU;;YhQKkBzbzWK2AT@G&E?tzO$y=1+tPcHH>6Kh*#V|}LFtPz%v^=Vk&#i3{?=~a@ozsj=42zD;l>?E^h zxJaIrtUI^OMY@Et)?1v5v^eM@k6HI8nRTivp!d)<)~37dA{)`)k~C#J*!qOvewx}&ap0?>p&N2>)|3cql=g(v5uXOi^N!6Bmmv3m#_}r zerLIUg!SKUILq+o&T{pOv;3CJMRv1po@aR%Szg0M$~1J5qu;s6O?+J2(M1MzXWgv< zF4Abcvz(sE8gpw|mujYT5+qL<^EP@dAHYD`o*wz*8$d+JK-#KE;~!e zUF7g_`(xIZw0D-)C0H}Ajy1QzKaSqpX$&D()z)g5F>mJV_;U0E+r<<1+{%zUQu;8&Gy_gG`|CU0O~g)XTq zx`50Bl{a^gyP>l9ipr7mtfhHMrSMUe?gx~yRb^g`O3w(DPobcQwjBl((`m~Y&m%JHBL6X~Y_@IbJ(4mO4q+2TpSGmXqAM z#+tOOiTe7olXSVtdagH|qz7xz9)e!7F0AdY6Jr}|pq_V<6RZ^*e#l9xCUHE*NtQ=A z@ok8c1cb3xE&OOC+XtPPx7JBU9l#cHWsvK{@$9>uqm?|dieJQI1dlf?S4j_edCnc(Fl3nw|r zW_Swg(e8W9t3nq=1{`4B?)@Trwv*cR!aWv|vI|A7&lMRnn?F5fvhMT@k%7>3=q=Rx zFzeNai46Wpn2SS%TYmE0WRVS?tj9i#B|is?)EmHOs$2^A^b=v~JNj89l(zfWNn66} z*=CU#{Uvvo=dM!{62wu{IF7h(9rtL=F$aXBUxo z&9PZs#J;r1lLD-%ofBPISm)YKWI?*4T!-@8vqW81*6hYNru3DLZHTiLu@@8RK>L(; zj&l1~M@hfQ+V7VgdG_Hb>(4k!JXGbpql~BRJC<9xhi@e-vCNGFYu3MaXE6M z>L{%vS${s5{hh2UzRgjtKoi*Z-R&r)nu^RX&-&dti2a2lbErCs515=GtS`UOQM{Ho z%9B}+QqbxsgZ=Tt&r#A$j?y^5k@Y25J3hNei|39~?4qOWjprD;PcC+pgUD1K=P33D zsK2B1?v2e}j#8tKqug+FlO0Do298p+37DtE0Pbm$qnz2{D90B#@*W314Rn-R%3Al09mTl{K9zKop@r}{ zA8XG;6$?7bky3fx{-VEf@c+67x^`&S-9At0Oqm+n@y|YUrc8@7Hu#t88?(#zH~z2M zGZ(hYWM@~Pd^x);ze|?4E0)8qn3?UYxiXGquKBG!(VRbjhCuju8u_>88?!4?`gdUF z9Cqpd;-5pu4EfAp<)ZyA6>e`JOGiumBE6PCeuXxYbzXzWw%eP2SHGb^4zq!xx zgkC)+v7(Vnj)ED|nKM0`^zGQC9^be_n+)Tb?Q-LNhW}2ZjK!=Y+M+A`ba`KEKk#8JZrj~P0C{HU=bewWXPH$(OKiQ`5) zw4_Iob@(Q0_}{Xh;YF#xi5ka^9Wlyl@-UC`!{`~WQDZ#H8;5%R-r``i!~4usRQ`-S zwCmWpeU}~`yLK7esbi0xb|p}dQ6R;j-~IfPZp_HXq@4W6KP&XV@7w;%egON2&wP5H z(K8v9m&-2YYBt*J>|Xu;KZVflTl9a{FRg#~!OpIYUE|z#jnk>4Kkt_eRUat!uV1^I z&)Ck$R>r?S>f?|7gNMFt{jp!N+_zf#WB#(4e^Q|tmNEhxV`n$tB`o(JG2W@Pf7rU`F`9Wnl(udisiPl7>$+NTdbeS`_N$qo z1I{Jtnk$Lw_g%8ix|XcB9wuvjON#ngQ?y*RRNY=UUO(E!YV*}mT4Z~au6-G;9@fuSqQNwQ} zYn7czdMG?e16QPI)pDsi5!n(IQ}qk{4!lw2RP`yIpsrsN)L1!Ldw!pw!>cE1v1iFz z?tQYJ#@@!#scKo3s_ErZb->zG9bYL`r$7(EAE-f2@Uh^~6y5wJQBAv&bSC*|STI%R z7bUKxsamjXs%8c|Yc;vWZWy|XtVz}3^^?`-VzT}^E=Bu2PS%~-Q?&s(90~T&{8T+l zPJS)}C&ph^rfMs6#KIS$WBrp9Jvcr^OL(W~p)9F7)I#p_q-slYnod4bilu7Kl5CTw zE!Z!afnAZRr`cZoE=Bi!O3}}DE z{8d+f)zx2h_1Ady*Ld~U@8Dm*gMVFL{dIly*Y(w3*X@5@xBvC`;9q|a{&k<>ulo#t z-Dmjg{?%Xium1VIXg}`D{r|czT84Y0Te)X9jKf5oQ zo%^bP+!wtI{=eQA9hdbV_eCe?`*vT{1KapbKi_uHRNNzwCuDO$jmqVvHHoyGYM zel^&(*xm;I3$zHD1b+Ca6rDIOMK4ZDQI}~c>H*e={#yZlE%>JRK8XHk0zM%^C<<7H*e8?B&xy%i=pMn2R>#F`oFkoF?CF|w4$@-T4 z|K#t%Si2Op=XuU9Xb9U4?NjtdmVaMYIgmM%mFE}8{|L8crhnmQv;FsVH5?neKPPKr z@N2=8%budG!KFY0!TwQKvyUhL{W(qx@{#m1Sx504Xm5J5UdPws*!2Qa7Co`>N6;U2 z^@V3?8TUnlphTV@HF*tBOID9h$@=L_vK|FH71|0t#)o3?ZYPt~>t?df;2B((JINaT zBw2U7N#>c`x94Q*!Dj|L8UIf~rNEuTb{yE>c)oQL%(Gj>^$0&7BK0fV)U`{pj_i@FoxA;K zb@j1dvW6h*<(jN7*{;zCZ2x~>S80QjwF2^y@Q*!{wVFHjz;37Q&z=wFYyXeugH1h> z)i5ksKSRCTlJzjW;D}`HKagj1fAW0rD*6X^Cq8WbG9Xz8VZ#9(E7_hsoVg1pM-B`&=ZKCH=s+{eTCg^94pFpMqM>)lgvwr$y%~kvX&qpU%>{F z%X@GSY#Fe>!;q}@U~a%a;KO^kd&6W+X_~A#!LFnK&a}lR@LS01J4Gt2sX> zY1P0ajoX@}i?$@`YM#@&(zXwM|8Bfmye~;pcPFVG_%HBsdy;esbRanCPwT4hlBB;s zcRn1Fq$T$z={R^NxCvcLLz8qa{Exc2IyLF<&z)!E(;{^53L-w_B!?a7 zgF-u!Gz1+EJTvc#jz?@CK!+KOH#SFuFCUPkITj>o|Mf|F9o$xE2is}elJrM>9|)gL zZf}9xiS37A^MES=A22gXo#rO#+lAz3X_CfnOwzEQk~Crie>ZPR(rI98V*4Zb`rz|| zi-MLy|4CgvI+duMAj4Un(cVl{pR0*_`c~q*0t_AN6tiGG5Wq(W5glCER;X~q| z*45Y(i8|+TqIR`Q(%fJxz07z|AyJKKiCXn}qNcq_)Mc-U`)}h_p4h~{*Ofc(e7NG@ z3~anW?krRh%v$8zqNgJ?@)bV+ZM+JNOZ>gA>^S}u-+m<~GuWp1+Yx>g`i%TN><5GW zj_s-VSL;Zk9yy+Xl3M1@Bcvpl9ZkB`4iPX=fAJ3FGUhHs4)433KmGzgHZ3{yt7mE&+DpMdcxn| zlNpNKfij7DpLbx+luXn{aCi2%R7}*5*Aw)`g@k{4PiCxLqSoMei#my#j;vSdM9l={ zsg$U3Q0elCI<&?=>T1o)gumC7V?oXvWM6_W3B{F5)WYycZ1rHfBG`!)zSUJJd*ogx z=uYx50=?IB;8!7RX2b;#!q*gJ!=TmZFo5j|b$XkiW$A|wpAs~UTxQ@mlf%~JB#1n9 z!hSS<&qH<%*qP|=itUN;WiJx+%9{k;Kz?#%O4Nnm((_WI^yLA3uR}f)iY4l3@Pnb& zWfOHadLCkbFLsyMp$}Y5d>@u2Q6GbU0Co!W6?_eBH^SCW_+ALiB=*li|6N@btd^kO z)f4nXNqj4lpcg77=&)J|dYrbd9Q*I;Dw%h%z993wUVm+FYCJA~8JulI*6Y9wJ;KtK9rBPvPS{^c}^IhVVl0zS!PEEKLi6DUqO=iY4gfD)>qs^U(*> z@qKgs1U-zM6ykJ%Cqd)dCTI+Hx5M8%(0`>9G#I*BEYBo-1N|1^hj6bOc`q>^iVc#GOXmW#Rv- zuF5ux*E;p%HMw5A9<3U$QYBtJ>cs1ix^UXMaO_{zRiW?W^sQe~*qd_`zcFdMNXs*VWl>@!HHWUiMF<3c&+IiueHJ7Y!k0NTF28j@tO%b!hTeqc+~>D|5q{o_xJkBwU1Xne2U>6 zzbjqh)#?K#MT1eV_On0en#PoFLWB4b2vT`3S$3$ z*?64_J+BGVo*8UT4#M?Ql?GjpRWN8CS+S5>}?`~7{e$Qh@X?c(%dskq5Duz6a>sTp5C5|byGCLQB+Wve(ngKV!R5b~~21b&r+ zCK7*I={P-{Cr($ij?+HKwy6=PDfn~&`F73V=;;leLVj!WINb%$4;Ny;Kpa!^Vi&&( z(Y~A*ZNxa9TrJ0DH>e`*ZJ<}^2>d=yBhk4Kdz*^I>ANCv8b{uDlE3=&PXswRLwwta zp)c}d@Sz^qUTi;uH-|THh|>c3;xv2NIIWET0r;N=wm!ZjVLOJLp2z=tU_-Ij1zZ*U z&B^vj>|VrXGcY#r6|tQU?0xJu#`mw}yaGNO@i7fP0qj}wRh7K{*SflsKUR}-$Lb>Z z`7E(oIcKbfeT~tn?_>0P-Z}0;+ZuSzFEJYUIOhMTt5$_LmM>Oo=ZV!jSz}eeZv@}f zK302Wi&Z=DwKK=+#8)vo=}wGJ%oY3h_mFE>iq)evWA!QAu3)Uz#0PtPc?ix0xp{1N z$s4QNpT_7su#y&|t&7BJJ#3^q#HwrESPf_#tH(;?Gk%<)Z7;rD1GBV1tX@U7<%1Z# z{Uk=Gyo}K*>|a1GAw5P{mW|cd;D#0^ZrY4c3dg4wi`AQurDUv@B94N@z3^d-Hbm}a z720yd>PTX3o1q7vZ^E59HnV7~K7%~jzgz;pkXZ))DYlCd*Fj`L$i-rOyh&_7<6i)H z#pX?DKC};+kC4Ng7_O6I^b|S!NFG<|n6z&}3};Ltn}BaBO#`?_EB|=oswg z1v>?*iftQq*MluYj-OyJ1RcxZ<=|5iqW|gr_SsjXHRXD=emoYf^N&PpALv|Sw3dvE z)(a`oS|T-C+d%2b(b^vDv!v*MdcQsMjc9F(jt3{BwJ+Ej(AWdfYJftaf`_Bk9jb@E z9Kq3g{&@87b=CN3w63@otw(rAe%0e>t$I3IhnV?h|=%~uEa^&+n;@=epmKfHN<9A@2(|#FT1^ldv&2<-|wIbMjV79Y8 z03EsaN9)Tde2OM+VooII0}n=PkE7B019(61#laiF*TG&Y{;mMu5F1ImqV-B-wC*9_ zouOpJWjl8W1_VK@lS)wL1(brhqyn| zPZjZV20kyx{#NWJKqJRQ{k^U#^Zmq8Ym|PpMd_*OQ95%=K4lXBpTKhz)8}@?0Egpo9 z2~m1uVU$*w8KrM&zYVqkJ{-mVQ1<5%%VO}Y(DTR=rA3Kr1h~`gQJNng!thm)?TAm~ z;5!yaX^UC-0N=E%+e#HSAr+moexVIv@WRMQI!{3i@ASuMzwben%re0bBFP z;RA4+;A7Z7Pab{&>q}cF+Ae!VsmIhPbtJ9^&;)$U7ZAmBk|=EmUx!R6eE*OrZQ=ob zF!9m{Lm+$n?+>;-xFy(r3bq@#Tu>AA4*@p?{;&_d>dzXsTAHd&qh}3hrB6*$?sYjrTSt2#iK2qbswf!2Q zYhFia={%A8u1TcU0DBH>wMLQZT@^p_Me-XFsky;-&l;%)`1G8SdWrViA0zb6%LuIl z_87hw$KD?7ov#fByhn*h-B2J>S0Gyx-0QsPgWQq%5u6SG*W<$xV$F=~akd?bMXDeB zo`oV+ksD0xi_kHT{haJSho|AsNV`bwfZu(|Q8>2!$xi@&n2^tjy{V8d+oi!wgqzXP z3G5npIpTlwggnyj%JC}LElx~LbKp05YzVe@;YjVtv9{1;a3#TQeiflX_&w%*gzotq zp+7>|i6ISreZZc>ulvNe5xp6>u3+YZ4MA6yhY{N6d4yI5`^%dMHNT5cFLHka+d=gG zO0b#1&H)n&J{Fz~Ie{&PulZkq0XHWtLTi1BPz%_7;By8U#_U+ICu(z?D0}6yH zP7Bwq8^d+U-f*4sQ@A!=8LqYk;Tk$OT#H!4b-pQFOL&E=uV=VcnHa83CWUL%)Nn1% z_Isaj?K&r15AF(A4G!0GyTf(qns6PzC|p0x4A(y9a7~6jgO35{04{P$xK4!ffh{y9 zT!ZI_Yo#6G8nz`|tF8;ze%r%!HvUwyh3g5hZoc7K*BiU=NB-g31E0D~4%aJV!#O9> zu{vDc7l!NO+2Lve+W~6I_EPY(Ea6%kdX1j4$a+D8!L1$_uCDmdW*N4A4A&dvU^BeR zQuKiP85^aihif~wz2SprgzH}DHoC&_``PGlor3I?CE>agzj~0Pz35&7w;@vl%w+a| zgHM5v09$`nxNgVJ>tW&gAVY@yY{rKf;B!OO@o_))Qo;CvxejI``v>6J4B@)YBV0=j z57#3T=#TN?x(HsEJj^4f_rRt?KO(;cx{r>h=pO?1QonGmGc;Vkj0o3OP(^${P2UAz z`#snOrR+fxfG^L&xUEEU&6Fhav0Y+VQM@Y zriI{nq11C>+U|Ck&dsu4H|5)})3fi_`p?63^TRN`cri>HgMD=-OivvO(`(3=Iu@qy z;nDDh96Ju5_*<9`v)`|q?e=T6SLnxw9Z$es4%5NdAA34X<4%TYCHM|>g@M}*t|WSH z+(ggYFwJs5OxxZH)1e>2bo~1;%|T4%z|Ol6rp3+^8#yAX%<0lKe*X=!j(z|{it7IMdzaZoz#yYR!D7^Ziz`2s&Ti#gDiS7C^A^bDX?$B zP6vA*x(K!qxYqb=hOQS3)lW@Bb$o|VJ=!Q#-&GIQtd&ExNbykZn?F=nE;Y>_asLe>as1)uFWKLGB{hX4roRtwGlz$V{$YqG$8FefqIX zs2aaS+UUtX9e~|A;CsB@r_(>|(_5eRsRw=O05%T)tALFJ>jCu#UmJXthx_#4<9!+j zeh=7_Z}#arusOj_1N)fR3xX|%?ITbUSZ8pTplS6(G<)|D%`z}VySRqvwYDMLs}0c+ zjY9N!)exOoIz(R=3DLN`A*w|~^ax~EI7GV^3(>VTL$r1C5UtrKL{|+6(L{Jj@P(U% z=x>!nG_+iZPKBBj57A&SKY}@qu8Z&kRYLSu*$}OZt-QTMbO^Q^ONf4KAHw~<5cMe& zqNgi_XnFW*EQ=zf6B zxyXNpRzi8OJ>kP%wSC>IzF=MKL-^)7M3?8JFTwT4{uZ!-#NP|-5BR+nipOqV@O{A^ z`m$FGV*3hM`)u?-c4s<-Xg9Ej@cBLX3t*Sw`x>xE!1f3Gyh^Z6=n$+g+#si5?bA3| ztJV$H0+oYxZjoS}Q8-wwIfJ!T=3wogJy`v71gksLAy=^eS|M1E*ALd#or85qAE;BX z)@_Eq>a-UP*4g=kby9&~9Si<0n0~o~wLLnHusyeYuo{X5YdZGEb_mv^?Sged(_sCf zMX;`_5v)531#4rlCG!Mx?H{Zc9D;RT-e4V{51+to!Z#Ou&(u6vKf(Ky!AJ0q@O>)B z&SJYR@+HANLq}_HKe6A8_EH6dbsRdH*A71Gl|5g_d)}f`o9j}Od9JmZ}g^W-P zG_Z6q-@ONGtuK4DAF*GC>VWNye~BEQNL(q6 zgPuX;v;#CdOR(0+N-n?(xHaG>f?Eic!siq3_UHj{u5b6~U2IoO+oRzb#P&tIU~L1o zJlH$D+pQtFcWX4ZBW?z1YpBk(Al;0-H@3a61nC6yS)T;y=m+Th7^E@TcWXQB zZF!7uS$1p3PeFPMpKIL+(tcnzg2@SaU^_p2-rXSma4$%cu|4BUkfy-Poek3OY5UCf zJutJ7^@Yk|_Y(9v1IPZC7eTrK-78NJ8{18w0mO3wnZ@w8=xKc}h--}?ZH{~`aOXJ| z25*Fa+i077AV|*;Q*L5=c?x-S9Xv^#Y}?;?HRO9?WT@nze4l@F|Hxj5@*}et6+`zF_ z=naFge=#votL+Wcm_317dVQeYK-PD5puWb=c;p{qGm5qz(0y<>mj~+7ErHs1U!bo0 zIZ&U|77!Sy71>^jzgv;p1Ln3hP>+Dm3Vk4kI{38!YDjElwg;-~vOoJz9T*ekP=cunLD*%W9H?a%$tLt7+N44gL<5B-R} zN$5B+fgIzX73%7ZzNzG5N}zUxt8bug_7Bwc=!%BMf-4DT;KV=;93QCl#|7$XFZ^YD z@0dVMMc1`Sfx2^Aptc7)8SDdSI(~P7eudmTh;3w`Zt@J&Iq=v~*dGfXoE;SOzgRl& zxSaR*j~llUj*K$0%F4>jI5NZgB|9@aJG*0Mo*W}boahh{5hPo`UQd6D*9vg6K)kwv4mIMnsd2nE!_VO#ulAkewWC42zQb(+FQjn1 zZh(1kg2*ohJoCkC8hBGBUcXg{*QrMFs)z3O*75v3DPCVd9Ov$6enr#3b;=vBD2I6M zf%lF$&n=$Uam4GdTJc(don^yMGhS^h@ybVhN9>R8L8o}N%^t4;W#bhI*OUC( z-1%^JCo4{We~;6oZ1MVs{T_G|xNAvHel!o^|MTZK9byhHj?Cm+oVI?6Q|_;Eas=-) zGmPbWjHV zJ$sy*y@^xXclaW{;R7^_z)yth@F`AZ;SNQ6KHLy+AO95k1+(FW{T8c@VX<;u9jiF>D$R&hxlq8mH5voROB@la#=~Oe1%Ep6>(E%` zq^AgSU(Jfu8uA0C#p=?KSPh&Is}&%foU{J;ABZ2~u0gSK2GOj?P?VK`qop8(o3$NFQa-zQc>>FW>r>oFi!3G6rZj@1y5 z9ez%Dp`53KQSfd6cWRFC;~9|qFYCabu{!CC&+f4*$=a2>4V-@f4)9u0BNLPxO#M#8 zuCcsknVG`*r88%&KY`wGE5p45lBrz<-#g&X1d(|6Y#*yd9hf!vsbj24bcxkcYILPu zksh%+-z!#g;klqY4)34fzlS>zZh@OIDwiIkd*5R80qegJj$e$1wqp*!J zYP~5&3u9vxMLY*@M@o$1&&BBQl^FF#FKKU#nxw|)FV?BlA0HE=QlNQsj3$DDNCPvpHsRjOI-44`S9ivzH>PSDU$cZ`-qcZ#P z2=@zhD@4WU?h5>ZMk`}f0)&F5@UBv)J9=y5>67)aZ816le&cRa1UYbO{28O4i5cwM z!|S>#Mzy#v1g5cW3@?QHI>ZvoV>D-J4DWwoUdv)s7p@m))8H0_`;`1QVA2omB{3RB zj*a|vf5hll_8F}EbCyD!0yhtJ$ze&{@3$b1&XD0gU#_JYlenG-eEgwZqn z-{3BRHw)dta5sb1@a*Wm`6pVX3#`|@qU*Ksd$jJd|LqxH&qu3w zYP9}29j)%CqSgFZw3Z!>)|MyH8vZU?=N;B7s^EJ4@-142A4lso+}>%?I({}<7Pu{r zN9!S+=ZB-U_h7WDrbKJ^ooKmQqqPIQFnBenbJrHF_(#!tdp%kk;XH%mN$!RH(aHs{ z<$-8fLEanDYX5h%3O+lV%fXE%@0PA?*|@&A2svC`AE*Nt@I4Xo!Ex`?_e$XOwJZ~b}#`>dTg}H zY>8Gg@+|SuS`)_{SSN!AoEIY=g;y8ee&C0n`Jf2=YccRQM(YE(Pn{KTbHZ5xuEHz) z|C-N;58yn49|&&-y5}B5Y0tMPx#e7|oAxN}x)Y`PSE4lbOq47qqjd6kl-?YQ((e6H z>I07Ljnch6Q7X7AN~NzyY4StzzC`IXHO{|`(v-isI~%2S$D;H%ypIQ?wEC|o6^B!K zca&NuMkxUlOpDStG*;b@QpHzMnnbi;ic$bI`<{!^I5?5XQ96!Jo9$8B3?6Qa($gR7 z)F`z(5ykhZqxiR1lyRaH9h>cPr$OLUSN9hx1H6@O6~?3FQ5|o*BWLOf)}gz<=`N9GoPl z1iZPdi&AS2cN3zc)P>xa#3RIspa)nBcP96X$juHXYIT(6ujM(AH+Nl>a61V8fF5&7`f|-V~O3#&m#Wt-5oRrUhp1+7KxGi z?{uUZU5u3T{zw&zkJQJgNOf8nsS4qdnz1ZW_ZCI!_XUxf84{`A=0(bDPNd%a8mZPX zk@^{a!{d>9bt+O<_mC4Esd%^_!y@%PG*ZVGQxopNxsghOH*r>^mV)DGoLUj7O!ST= zMe5U*NOj}vr%jO>6&b0)Xce2!J)Dy>BNYvv&xq7rFzC-n-K74V)sdRDK2qP7Mrt2g zo@ftV6R9(3Et?&wZRGY2id5t@`k5Z7#-JT_d;A`$`uLiGPK_mzT2JmO`2L)az}tUt zc20>@6_8_Uq^5%lAdFe`TZn(^6~e>b<&m-mM=A?WGPOTV#>b>c<((KQcd!aLb9RwB zP0(>;c5hg}#={)yG^b8&>OLdq%!EkI{w0!sqtQQD0b)2`4=Pe~0Wk-d11FLFLvm}8 zbCvivSORiT<0a99^8hf0ya%Hr^?n?+I3E-ksnO#jwUWFa8oh{_oRN>|4Nu@Sy{{v4s+=!|a{AqVQ@X1@wQquWNPAU?~X)s5U3_XxgEOAdGp zs^cY{`-p}SdJTVX;|NWtL2cq={H|qv3(opV5t;(3g4ZBB+y!0{>W;5P+z((ro4{e_ zVPO{C;Q5mGt$c*0fvOcEWCa5%Mra}D$@n{6H$sytM`#o?If2$^YUZa_L+;j1N-@zqv3J|^EyrSm6>43g);qu_5LGHd3FRVGmb>P_ z%6(yY4Z(BvtH|*MrC6te(X1`t1qdn0^DPshePz+3#%t~aIFALb!4;4Q{5W4moJsT{ z27xM%!u8=(xaNEfSJRi_a=ja_QrE*(_ENZppAFZ!GvSIp7Or-O!+8y0xLzC#S0iwH zf4KVI2-js>xZb}DSH+BQHGdwiA)Ix;5-tne4JX3Y?_{{z!##B*CR{KuO4c+lyI%3=4ZTlC84`JT%&h!Ps|1G z{or%%#vEhlsT}K8)LDhre*DeD(^u*rAU7EN1Zu$_2*$ED@B_F%1e$>naH-dsUD`4)xxwKR3onv{AUBgG-6Sp>Qfy3Dd+MXd0&O4Z>92gz0{_Fm0mF6xQ+W!_?n9ObhFT z={CCc;q7$~(+W8LAQmj4?n~}UQsWdJwh?Ed_X+Q_+JtE%cdO7kNbbiCI03gJaXqmK(Y;KVek&KIr>{o{JTFg-G`8`yQ3pgkD6r2&WLse{6sJ6`v z<#Q%Nb#hUtRz-&DOnj&cZ4A|0xW!^ZbqD^l`JwuPUQW2<$e#o650D>xT^Ope+^?gC zKb(EbLluu+65bcD#y@^b!Rt@%*y*AAJ|mRZjfScO`%U;C#r?)m`Xe_7HOjGf34^;h zR1e^FL$4jYAz&^DfO`YXK;t$&wWW_J=5q<3kMWTMJb{-B-ri}U8XOeL_rODW-55FG z4mHLsWwvl{;9(^?!)ea0Q3X*K^EFI$h$*Kg%eMWbZV}I zmrQg86Ulo5_ZP60buxI%d9TT=r-teS>)h}M5l6#2K#h~^zp=NH+n(5zSdiF)Sf4m! zVyIe83DtCJ{-`$)UJ95Beg)Owj{-fxkM(*u!@*-P^L~h;--jsY=MWvRhiHKEJRGcq8!>4%=qOR0C0x$Y-h(^M>urEXt;5^^Y z8Z_dUG97~Z?tF+g-U`tmd_>dR(+eSbP2E;_9YKC)c=3OQXbsT^Ooe+Dja_)|OU`CA zOW`x}WQg*hmz}fU;9Z6DM{eF2He?M29|Cy^dFUbexp3hYfxW)b1 z4|6q^+&zWosaioIUVIA7)5Yxb^zrRn6-Kj;eXdGBo2#K<7g!Hg!8`kIu5M<|)g$zW zk>C02T=_Z7Q@Wj1c;7NwpYz4RAzRTtg|@+*Qf59ewQaUOUK z#?kKzbiJrw>f>BZ0K=&>o>`Q`XBZmQ!4H0SkO!;;e}frtih#OkWWu=v=N*0?qIUq~ z`k@QA1G#lUd)BjoFF75EN4PHoXA(LSSQn(uHP+u*y8##S63IIZtRS4dE9Xn-VGH|1 z?02&N3g>s~lm?esF9+8-&qfR*RwYJ)M%?eH6s%kggLSlNutwDh*754WT3IDnBPs@~ zo@=n&O9$(ZBEkG@Fjz-Gp#s7BJ72K=1x>hbSs_@Z>jmp|qhRf=6|5+CIOT(Nxn!{1 zoPt%XaIiASH4ec#kSAE#K+D|0ddq#SqQUCq7OXNJ!TgSiV0FdIM6^GY3D$1~gB6lL zSQYaIYk(zK?Q;cdDsW<*6JEMwu)<0Ot1LdNl7Fx`dGODcMFZY-G^&srmpxd%IfB&- z{0b`2!_Y#(YTz8KVQ9ZC5v*|h?WN{a&d1_rPnMola5CGQ?O z`@lrj^NB5(nDLE01+q@cAydKD7H`$~dTX=;#ajSo`&;Xw)>7^E`3LGo!6q#3P)G}1dr zXF*!aAkF2zD|x*Z1?lwiAYKa|q^A>uRC;8PO8N!qWal7dZ^ybtkiItwQaNzGQIH-r z4$`Q;K^n?^!BIh)g=Uu-K}x08ZTQXngLuzvkUBIEQX{V*ef11dg9bsmTR%uOKwQTl zO>G;bY6F5)VQi2_kpH+_ka~;@;^*f<>ISE0!yvt=2e)RB#?}tfPql)y8dzHeX|RG+ zoSt%y2vW@+06$%+dC3In9-JxUrPK)Gce(`00v>|m>>cT8REHq_jR)6$`0vkM(;y|# zQ&e4a$yrl1NSED%bR3kZ5~SZj0kEM@kj_y5W}P6dZy%&L)VWGeSMfI215{%UASb!^ z;r>xMNU_AbAdOyzl3NXL4SvQi^Vm&aXF)!21>R$Hz2HtE-vQKMeFDVb$}*&_IR71*%d~pdPIcROwZL@>>?Dsfz+NYEGb>W(Mla*g*X|JW!s)0(EFGxnLms zoxcU@19`3+0yQilP~D>gb#6(ZvN+G29H=$$Dvt_O(x5;!8W^Z35Z^yg@()yzsevjv zE>OGX2kK@-pe`&A)Y*lBy0SRbDfA~d?!Qe6)X(TY#7{K*mDDdt z{sW&txqv5a;e$i0U$qNVKj!h4`roPJOpPMwU4pZj{R4Pc$r}w$zuTH>^wHw^0hJ71mM4ST7!F|Db8abQbhNJVMeW2#zzbE*K z^Juu~Xq|?a1uU!^fNk*ZQsWdcfxI8wQgEJu#jNjBCp);z{TA|Op|t=MVBedXr#A*D z_F#a%o(WK{eF2)dGe8rz1}GsWKzG*!sPjsGp=fA;O8*uh_qhRlEd^*fan^zW?c)A0 zd8zPRj|XU6Qh=g12dHgSfJ%i0=<~t=g@=&$Yk>C84$#?I0r~`v2M4I<(f~cZxwH9+Huu^s^6lfwg49ex%u|L+0H zz9B%_@H=%{fXYn{Q2U7iYVZqjLV*5ey%Ge#??Bz$^8<7qox$+-j1SPy)O1F(D7hJ9 z1GH>RfP6tS)~&|{r~o+u%%KmuIXJ(+I6#l-@g+6upfTqMXJml<0|FF0B7pbX1*j$H z!r4!w>ERE2&kWE)&TQy6LhmJY`f)dY6urR70Xl)i;hcfqKhDTM5E!6?k8fFR%f4bKjivcbqR~eU7}3AQ$L?=3uabwIg>0$=Na_Kqo+d z)>Xkv&O-+Us4Qz|)@`XZ480BD7jS}oHqKAM84Ml`rZ;pKa(0Ki{>S~*-|8>RV}I4T z;jfXG{8jFpzalwrf5@M|>-#Hehrgz7@z3X=lcTw%AMe^CEOh+@8&gl_sG5K z&(CxG6#y@Ex4*8C^M1R(dTl0ulfO2{`s*0+M4Z1esToDhl1I_G={Z;pvzmiV+ z>(pQVTDs9+2SJGq{u;E-Uq_?;`MHt5z5)+wEaJS-9)GPm?5_sIMO*zfmRcwF`Kw)& zzv5T>s~ecK%3mvqU%{TW{>sVS1?nBb%lIUJUSr{}w&DJ2hQ|rieXzn`Gye3~B`}k9 zBGE+nYvM|O4W*Zn=x$izui^A&-H9$aXUIui=C2!J{^}04{^73*;4AC0oV#=XF413J z=uQarS1D?YK+A6_Yiex)-$5NPez8BlC(B>Ifd{~iocHjZsn-c^cj^Uj-er-$0*TAX zKg{`PaGkTmzcX*v1Grm>{_hLX=ByZJrNDl2?r~R*y4m15aW;+n(O?F-W4L$X{216f zpYsrZ_2cY0s0gnGKBjT^gL{axs^Abg*Wmh6V-WSSllS(mpC;w(tx}G?wV!Cs^y706 z{50jgpX&ePr`F)+Q$Njr=%*r2{8Wzk3iM*V^M#*gyz}ETC3|Z|zTTRZqqjzX@Y5hT z4siS*`>7>)|K9b}-8+6Nf6q_nz!_0ozsFfWWpEdM#82b7%gcHt>lAzhvHo`4PanWd_HBtFKknfNz#nvo z=YVEs@&_FC)36_roQ~XYA+IFs{P5~>w+?JzJ(lzK=#)C>r%!PIJq(}o&*To~?r&l= zxJUj3kOc07jpXd$ei+=_jeJ$2ov+6A@YSOBzFKd5)u)xO4mI_aY3$4Asrc$u17AI= z>#N5^M{vc{m!EI>%3jA;E4aT0|Anuwmc#dI?yD! zvkKLH)u6JkKK<;==lJ-lYHMGurIu4mUrlT1tITS^!&g4U_Emi~iu}nneN_s6V>e&T za7B;06W|-pdXTfGnXj6#cdmq9d33>u3cmV=&blhT%0;b=622;4)>p?kkH_P7&TAD% z6aFGlpSxM5sSAS2`RXM)#mU`O(O1*C4`cth5c9%MAi4+1TU^RliPUXY%vTqvWo7-f zq_4s`cjUeaHD}cDRZh6!Kl!R1{$0@>=;W&nMSZoy8Go$rz>ft(;s2=dA2p7-_^LED z`;#{fT~F363j69yL0_d7@zo@FJKG8|R}lVCOa;H6`aaqA&FpgQ4WySmUGdt9(?6+`eeW zqS>CiWNOdktjq=<)sFR1Zs5IADUJzMIdgMaww00?Crfxnm3s*TQ{kv=L+ejd2* z=lbXYb^oE(#84mJ1Hf#FkMK1#j2@}?AN(W4`Q*FdBM<&A{O+UjAwEiCy_Gm@zK^yn z#5deloGk#g(0)yw74V1QX)FBbU}lfzZStSbW1hrW)H($(2b`D0Gej#0hx>&ZE#T+E z`>xqO%Kn>==D~XbvcsE;-xVN{{2p+Z!f|2!ikJnrBfcBJ?cdH@8+&?d&0ueR9q6q{ zkO;i`d&}0xTY36r`G26dX7u(}D^QbqSN**;fxJv=EN=x`daHIfZrFNU%~V=n!F+MRvU8KQez%@t*F*cK- zc*7V{^%Y9VetF$Tt348 zlb(*%@K&AL-g-*@dH7?&g@)ex8(!Ha%pC3@yvL&b7=9tR%g|52`?KoaYFUdrJdgJD zRyw-h;66q77@l9^`5*YN;qp7`6^*tB+(~e~YI&(dBQM3Z@lsMNFV!ZxxAjtn@zSx@ zUUDP`f!{eh)X7T`zFzvRyO)ah^3r|}FD2CTQfzZCJ>ygDE`rU}XyWaq<;}cw3pArf z4fs($Uiu9GV`nezfj^*zm&RA~;`6_})Si4tc!Q`jp8O2r6V83$&uIC-nj_nJsV?04 zUA?3#Uh41eC09=`23~x|nHPVqdg(Yd`cU%#{{QLVrOFk& zWQV&4@4nZN3KXW*wcHAmL>(mH(gZ|tSqXxp8= zRLccDxJ&T87G57}JjY8xbVJcSi|04=eFy%Jne;&41?@4#h$X#vA0VFK+0Z?OW@oq? z;ku#gj&41;1<!~{vJymszrxJoZ^?ACdDs&~s&r_|2upjBEe&kmrUZ7UeP*2SV138O;zctWP{*yd4 z1b*l=a(q2iv9qU6p`91qf8bOhe+>Ll!#ve|kf$mU>#}bKzvC~SDh#&;+Pgb?YE=hM zO-DQ3AKm_*3S=EZeiXG1u;0pg7wXle?lruBLEjnuQEffd6MiZBUf9=DuDw0w)`=Rd zHx2NVFR=u1A^Pj6*9QM{;g^Je9o}0o&UmV6Pfy;X;HhHNX$bE3zc>C}?x3#A_(R^`$6gMp0dGx!Yp$9&}E+fXkX)5T!#PA+mqL6 zcxo81wnwX-r*it>5w0KH8*mH5jYIcW_#@%|jP@|N*_wE2Mhj2Xg4blgMzk9w#AvHK|xIiL2>0-|H82d@|M(4XvcT=Y=x%O1LT#Y4W= zJoJ8>hpO%JP*948YLH_&>7j)uJTw(Nq1FuWoU@W}zn}9^LYjxBz_-A4-tM7tTRc?i zFAt3&Z~Za2N6;XDJL@ZlJoJEA3=b9IPq^Tro#<}4>Y>(G{e^IYC+8goi0RL8uhf?8sCVJ@KBoDRSOWsxwO~Xg~gC6>IzlWNm zeI8HE(av|qL(9*5s65{5!L!9P4{Chh>A}y0J=A3f-dW#6|2uPhi1r@(urq_@%%tX7 zW&wZsW)E!whoU`{o!;-EdlTI>)?W0{5$<$!8=;#U&nw|>Li;w}W1~D&8s5B0aT!Z_N`A6aX0Njdj|E1?6_-+Tc7`kaInAK_z4THZO-(5F(Xal-Y^uHc%G~6Wm zZjEOrxF^xRiFRdlYr-7@H)frOet}yDZGX6*(4BzifAL%zt{dF1%zhBuqNhFdmY%0J za@WaL?(*;Ct}*T1)xU$gs`|QXPFHvBC0-@w?#8~myIS;i*WteI+TPz?EB)QIp@F+D zdvVs*U4G;~>g=v2U~(6Cm7|^m7!Cg>+zq|l`CSq2`n4am;I?b%uFszCy4KQNGtr&c z(Vg!FyQ>jgKWfZlJ&pBKw14U8&hMFb=Y3J`DmchpZ|l0NL_K%yZ|2UQPwwj0n)?=b zN3STH1RroBZ50lAfdBuCB~8sOGLgaA)H^5EKGmes)(S zxbbka!R-sTC$s*f@zH8QT=kr(bjPKQWu))27=atNSSvS5G z;Qog1k#Gl>b61Co?tJ!zJFgM`pXLj=_3>XLY`Q2pu$xT+U$kC0@`E*lOVK+@J=BC{x+*GQRn?{#)<8`90YW~qx z<1B9KO75Y2Zkh;pRslEFBVGpU;o6G0sR7&P2Dx&Fw{!n5g z>a>D?nSFhQxv#FOo#CnsYTV9EE*?JNyC(f!!gD$N zW-I5WpUS)Gq1{y{(A`Ll+@D&ARuy6X2= zuKM=WRVV4aJe(rv?u2h){;%Nv1NSQ2JjLDk9)p`6z-eBefurU&}1c*~XF0RW!3Y7V}u;vpLSPv~A^W~+0$X)<$r zThL8*<~0fJtN89o&ki?Twf>%~{y@7d+{Wmxfg8!}2Ev_?gJ;I93p4Z0_!d2vKzkd$ z6YsdF^Gg?1OLtMZe_izQjSHU#@1lUuE;iDLu?viq8}& zrG#59YW%>3-vQ*JgpV#trbb|fi&nA@1J^#eXzVu^P5$npCU6_&DW!rArL+-lznd<~ z4&Fa?(JyF5zjKibXbR^s+{bvR0@o{BDdmS72X`sFB95ihG2+5^|2U@2`VTuO7VxM(%ntuDLh<9!!7Ja^>y>(GT^ozm0 zj_0?`sn}1YR2c3zbPK}oe-VA=b{Fqo@z4Z*8T`Ly23z6gfSU-{M!(bWyaCU-Pq}F6 zSr`3!!A0}Xe~SOMR(vx1#(1zY|IWk_dKFYZpDE=gnVu253)3I}B}SwA-X`cEW}4%erU+{)?jhfM+(9dEI^N zqLFqN8D{nl?K^ypRA=6AnD-g9hrM-@N0t+xpX98G7H8GZ?X0u;oVCB8vz9tKE3>e( ze4U)tvA8pznd8iV^Ulg$k$ZP%*v%> zStXM}rIB7X?M>c0!;Fn--c6_Ioa+VMMhE#Ue;Oh8Bw=evEA2_Kgb8~`sj@eiL z=%j+oe{?QqwZr>MxT!AA8dlC(bD4ARs?NH1-AS)+JE{47CuPI?<`+(UE|n9n=W^nC zI_V?xpFq#9MVwU+-+s(%8`@*gPQB!$MEDo((&IxXHGJx%2=qhXw*2CxfAF1-?>_i0 zhxTITeU_O$W!_O2omA_Zlm0+E3*S@V?nZYnvkS-fIA%YMXSa;GKV)W|oShYf_j`B` zsqLs{%^mfyt)s@Yanw&>QF}+-=;WxFE{-bSo%3Ffn%~z^PJW;ph{%utpbsFCn&@RvX-r~CqzLzqy7_^;vR#QeeYTZxt1^-q_M>&?o zOL_9DIr6?L?wEbwW{%p_#!*4cZB)C)Q4clu80#xsKdV1T0*L_4Twf`h`69Mo=?gDhZJvV(>maL|K84*cxM zLHSNOXzf`CMO=2!&(|H)`?iC|K5$U-Y6oSmb5Q+62fc;UpBhE?I_N6ve)}900{_(s z2mJwe#3gFN&34N{q4&W`2YrZeQ1uuGok9O4I76MS)L0Jx7W`vqUp(!g^%or!e9b|t z(QS%ut3MqydAWnSpj|80K`EOY_-q*mO~bp-|NDqcagYt)C(h&LDqhen0k^;(4r&Ac z>M93?(|gtR4k{DnApb26>al~nJr24??{$wmsNy*X-K6L1RtH7i!z0`~OX-*RInqIm zHaMsr9`@3M3;fCO5Bx>$5eMB*b!6*~s13i{W;E%) zA>L2m`^-uDMBB#9EH@lf<2Sr5aNxBU4tf~Ib6Dn}bM)|h4IVc-=-pNaet#I;{SNva z?Ys0no4yxZL2ssmX3TNWV)$3l_P~20`nPy?yXk#moP+*LbWjWCKH{K*W-{~fct3u@ zK`Uo4(_jZRnD3zZXm5mjCEP)eBOH_$-(#7%?=}YoG53=6y@Tid2)@S|w3=G9xt&Gf zz83B5Xi>HH7P)k`sCrk6u6MVnUT=%W_qXWmV2f6bw5aoVi?&U&=-D)j5*t~Rw~0kl z;2(v*0NzyivpR9!#iBpES@gJ%MP`sihelXbdYna%ez7Pih@1u%b@H?*3jJd~7By&V zQJS%+N(YN-;Uk$^zx1-mGSH%gp%!HuL+uF`eV&TXx@gt0@VgoC)!d?6U^{i%QlmTi z9$*Xp@1gC5_5gf41zHp~#lm|SE!tn(qMP;c;AK%jV~a-MWk(B(y0x(=g&OD3c0s!| z+S!I%c&&^@W$5_~+)K4AYE{FciSSRu-@pu35=+5v1OE~Juh6$!UyH7y-7UbPuk<~? znnlClJ*;Zc8uX*##nFG4hRg}?pIV`Z?-YC&VCIbnSoGfziy}r@r1BOOugrZFi&Cjk zsHR2D@sWUsGidknw(xIqi-Pfeusd_5?^}4^HO!(v%2@Qx)uIWN=o|ibd^AM=Bis`7 zUJc)^nfuT9?#VMe-P59~c+c=>2Bj_9TaIU4!J;?r^jY1aCFnM)M-M-qeKQNc--R=t zpOt6cg}zTQw{iX1J7<}B)w4`o!z{C^W|m2-oMl?GPpp+?D%8s|&AhVA$W~cqZO1J0 zq)(QaK0M2u4$LyOW@Z_md|75};VknP`7;{8t(|2GRLwHw*{6V(b+gQyW?3e^ZI*f2 zBg;e#$})XNXPN(|WSMI@v&@EUS!N6T+wNKB5Xj@2WiAp=SIIKlsk0kTOcm zgML}2;HWH9Z*rCyl9g#%e9JVi9kPsP(JZqOY%7>$0^u*Nm}TC%WtsZ;pWGzN)bzXl`74aqY3f5|fIKV+Jx|7Dtu*|SUw{8!4KWh%3tUL?!xD4AtumdP?t@$jL3mT8an z#m@NX572J>G1DY|&NR>7W}2mVPcD#UmgLGZ7xHGA;^;RkPMr!_W)^e!p?#CS-*n3| zJN>iFP&jqpXPSBcWSTplGRV|e+$qb9!*@=e!T6ui^UgA5J7k$?ygz-I zX}Ud!_cGIXP~$fEB|X!GFoy$Zhv21W*N#dnb$MTo78Abnbn~=WhI!(eVHTCn zFw+ZVn6SbbCca39c}$I3ZW*RqwG8voGs6sQmSJY{6QVD^8RqW0bYpp)ZtiELn`KTJ zCJ*_^`7%rf{GS{%%w=N7QW<7{r3~|^W`@b{m0>ou$uLVhWtivobW`eOx+(M_-K@)= zVG3lXo1`!4X0$_w3I8d>cv9!|kD6%zj&_X(=r+$VbK0T%DBYOn=_cTxbmIX(8vO7P z^EKTZq*h<%(E1GhUbYuNH zy$nS=mA>PdM{%^n>SdS~O*2fk2kFKWOu3nE*5hIIn{+ejS-L4eFNfc!oBrR@jdLDm zz#PQP>(KX0X8sWGIj^Lf&}-?Y{*82#3eV~3<}SV;qP+p_<@6m<=6}zudpo0>wh{q1JOXuGL6*=|bAvYXrs?B@Q@c9X4z-Sq2f zH)s0V&Cp(Uv!p#a)NJEpH;bs_&)L@jc5{A&-E`W;Yuu*v$@Ve5zwN0UmafQPFNX zdD_iP`guydKhdvDPksB_&4FR~9%nZLrr6DKc!S}Gmf)^3e$a1P)o%U+ZZ)~X$F-Jr zW6-`r-!`<12H1_2zF(KIo8jPtlige(R)sUc)ov20H;6fmt!p<~O_?Qgo8H}Soch^K znW1)5hnepx#Jr2z&EG|sE83-s+s%B?q#V8D;~Uz4G_spkt?cG3+Q0G~n=tcc!|kSj zKD(I+<`uLXQ70dDI+SECXb+|D)avv~?+ehLVeDob&ms-)K72il$%79EW>mm#eBn2Q z+ZFxhrR-)?dA>H7p*u4i!`xh$`OpO*d^O70r9+ZD!lw zHe)|$GY?o_Kzk@P*FUkDLI2_j?hUjXAFvtAUYqHgLOg9VaX%upTEQt#y*;OFrXF58KS*qwuJ)<(kdRq_45h zZD#OWn^}c+X*4VEuo>$no9VgJX09dM%!@>u8OC}A^~zGifxh}Om)W;$X5S;5$-`{Z z(RL)qb*s&MTyHZQ;%&wY4@Zd0fDbk1U^9E+om*!! z9x*mEkbQJCvyZizQ`>B&|86`Uw3!)b4`%k&tvo}U&Fsf}tu;1tAkt=9jXXzuTN0QX zeJ|m8)wDpb75t1Sw746v(Q1C*W;NN+ zc!l=&BUUpL?Ht_Oms!n6a(bfGc7@g4USu`R*ICVYbW5;bjdpEnFGIU%veoRu-wb+f zztn1e{ljXi5jRE9W2n_6{%SQHiT^CIn#?e(S-#3@yke}T&~~ed+=D*8ryaMNdn>Kx zKl0qj??LQ9oi=l==D=@Ovx?Xn4~OvOg1;X0?YINaXa}Pm!}+cGRx^7B^PWxJ5Ucq% zllcMPIaYIwyG7IsLAx*7+nD(*=JJ@n-MOz1C;K$3xi;NuzD}XWWUHyfI%AgAoI}6U z?^cr#ZZ#>9JU{psndd3ywu$;fr&>+pNmk=A!D^f*GDG%L@lk=hH}n#WwgbI?`;!?) zSxr6oQ?}uQ`rFWX`is^01zOE7@H?@e4SzWNjd)s#cHBZ{i1&-k{WJV9zGkXrq?u)( z(@Y^^{>N$N>(w-~;e49ubu!JYJ(_0Hj&XlH&2&ypGw&~^nQq)W-Aps5@1>ckPt(k$ zhiNA3d7A0*B+U%8rWxO}X{Irp_xsb#p9j)R7&Ycm%afW%E~S~~*V4?)TWRJw7|GeE zD{1EF4Qg^%6rD5V-i0?JDb0*M^uJnH(2l%-2edbn>zI~iY~+S>UgS)g=|ukf9r#R1 zGu2uD$$DmTnyG*If7<8K`R6h@*WuyqAa!SuH=O&1yVFee4QZyrwluRFwA-F$u5dPu z`!qCOQQwZYw)pOcUMbG^Z%#9V_oSI(c$^WNW@6xX1Y=m|PGFXtFU3odQ)wptT$*vA z$G+&sQ8#I0nyDC{W@@74vM$YxTa#u&iJ38J#&1iSd9jN-H2N|(%V}oI%)8Rh+Bkfz zXKvBV8P0-;G&6a1n&}diX4Z2y5AE*Mtb#@t_{s2-@jj566Yv&K-iKAp2%eRA5AFxh zjq{h>=b)#R@aNLkG@jjg{1(9H6KbC)Z#}#mE7ME^aFz9lNNV6CjQfA^z8!yo%x(et zjSu2|Y^n(!pK2P9N;S^CQcZi4YQmbNnoo68&5IhTCIU39o@%mtq?$vVee+B;0~@BA z^jiR!uc?sk5Y3s;OB! z)zqWL`0lBuLz`4ns$Hsi0!}tZuTH92$^Di}spe&wRP#r9>Qbi)HF}^Q$6eccsb-eQ zLH}UWRFe;{e^L8=*;MmqH9S>JH8o1&t8}Wl?nXYf4s!mAoMO~p2=7$`ywb}tkVgL3 zLaAmEoW{jdjbBmf6Z2B9A9uT}Fb}k|!`;{d-*Cj;3Op?b6`fMe_kyV=R{>@PGK$ax zwVYj3%_FqOp}iKqGd`O#M>p<9(p#Z&_#}UC-c(c0A=OmNpK9vjqf@a|^AGnk@&2CP zU&GH2Zxi|BoKwv`>JKOPZ7zBQuXCrGzw@M;%G3(tE-&5>)BDfN&IRv7c?Lcuc!q_U zMLwPdxg9`DV1<7f{uey_^;4=DL(T*A%fsJ;{tnOt&3yFNg`BeF{|xr!Of?0G)j=Y4 zO5p!AIm_Yqh3||1kQ#^0JCDPrp!;ECbvkUinPR??`|@gv8E_-TtO0Mp zv|A}Afb)Z3cD}=AXC@jT(qlA4|ibp!)8f3Yy8{j z-;X&gy_{nHfpg?Uidl6$#e6)KV%ov4js8q}NQYk(epS4>Gt)5YB;vILh(4QQRvt?+ zi;tw3W1v4Z%5j#4cHb-Xh4&@&{vBRte6D6a3r|(aDR?x+v;fyrQp~8s^ve1&{4RKL zrk^nQAJI?cS-fK=m)Wo4ZVWj`@Y#oaC%8KgrI=RmE$pA1vLXl4<=S z$rP|9neaPF<|k_se|{yIpc_eM2;p-l2VCz-J4NoG7AE8R;n3HZEoD#^S% znPl>vPBM>Dlgy?wNoEoF0(|gz9ZuZ)By;jjlIcm0ju(>53w-S-|Jczao)LAACz&hM z*#y$4u?Wn~k!(INn{{wTKIQ%)n)LDuT15^gne+omrt3lSSht4Xlk=&-gBn?Qb!RsJ zGS98x>+K}-nVPl8?F8q@K5D{mk&ktA~g zUZwp>rVZRuU^D!Mhq!~EAN`l`-@Z&TgPF??p2Na(Nv0p?@8AwYXZ>GE=6q6;*$qx6 zCz&m=8GQHs*#%p(;TXtr%i?g=e`=Zr>{IPrR02;t=O|5j(Nkt5OgWhLx z1e3o@qS@0n(X3IT8QeV4guv}kI?>E3lxSAuPBgo}CzufO7kx=EojxU)e?BCb4jBn% zQhI_J1Sh3oqAA=c(OByxnv6<`#;aJO83uQTCDG)BGxcMF$wPk2cL`<%m<>k#n_vz% zNi&MjyiRx>ukN&XJ@ztQx3i9Co&F+TZ`RnTGyFF4GlPx#tkgz5fnNX_$I)9Q{KBa{ z`Yc{wrN-;-=ke-`KBC;Hl>bKEP0k(#$Vt{K`N_^)K9R9D``>&TuaBO@YaP}NDe*d# zJqDl%t^>$B$!DW>@}}3^8+Dx5Mji1bUgPPfB72vIb3cmL-mJ^um!dQj_meem)D84F zAEqpGUBK-1O}u8p^QT9z$MIShcG<&toy{7_9*MAdV2jh^-h3PNceon#xt*-_nd5F| zQI?u-FqI#`piSr#{7Lp3%DZ_bHtM0g8&x=u?VQJX&Mk%coMzvB)byeL@8ozrb1z

4GgT{raCvug!6()9%s_V_e#Oh)Zt|aA|`u)Xk+gy0~pCKh;->_a{kmgdg}w16OR(qcN3SMg2{rynz*!})1{B2Tv}!<@9}Ih+NB}A zUHY=KOJC8`0DM+cdzsqyXf68Cm~7}0dD~9KgW0qk?$VJ%T{^3~OCK_mx%9U4cbDdZ z?cCI*InXB`KQ>wC^&6Mf#na{^ozxbf6~L^FPK={U6iPnVt;MQ;OLI=Gih z`_t#&!7lxT=M?f*A!j<=9csPd_OP~Mt&0}4h}HScV)dUUvFfnIs-;YMl(`kv?+|wgBaa;H%9y4iP6$LEN=z%Kk6qdi~6 z=!n!94S5)&u`nlZ$LQ@_WVs%rzmaoMxmZ0&R(r`QIZWQGF?#lLjCxdz)nmS~I;424UijIkXRMBf>GUy1yV7Gm`Z|i&U6^dwV$=qc zd?`lfUyM;V>?Hhh;g>6KtakT`)zf7C0@w0MjJ79ddHmL1!8?h5;X*FN=&$Ev)LfqV z7LC;i{Ep>_)jZi_weR~FT}h8Y^yp94YOrCjr(lQR_b*(-b20jotY7oTYPOuQx;}HP z-XL$@7i8ruuH28&DDuuC?;Eo2hx0(QU>CqDo<+!eGi$8o{uZP1Hb(2iejsndr6sL~9fr>ZF52qU$MH^euXU>DYNyUv z<Lm6R=()e zmIs_#53U|rgX!;doKp|L=@O@|gt@!GsmuRy>ZMms{ypQ=>{r+e&v5b%IPBEYJDu8X zgHy{hliuX)2j>U36lTjjr?#KV+{k+`*{M~TYnKa7zISx$#Jx`4zS*h0);ab43a1u> zeGhvLb}DRn*xIli9y;~l4X0i|@6;=&I2-cz-|p15%qL=vQ|rKXVm3u#S1feuFR+VX z$HKO{=hQq`oVxoA?>Ljpdz?Dv|JZEI=sq(V0b30=AHLn_bsKCIG>-ZBUv{b$&jilo z&2BulI5mVB{XBn9&e3m~Q+F(OYDwms1Mib~KcCNBvdq<)KO9=`i$i^$JM`Nfho&Yu zwCgE{zC7g6y}KPcYO_Ogu5+lr%b~fKJG5VnL*FfR==pCBz3|?l?cO?c+%t!+gzb9X zp_XF~UAxDj2e&vhZM{QdRyy=cEc^ZI(BBsE>Oa^L#Qr#i2`|IP~cQ-d%HO2RuWM zI`r5shi={A&_Vdc!_`{s(2Ru+-MGM^-{(2>@JokIhVe>vXx*C*J#m(O4mdRWphKJQ zpbl<7+|nfuP5*~%Fr^$0eLLHsl^;9w{av2jaA=t;4y|&My>~ja4{8$c(4aVnb|GgV ze#PNd!CZ=VXr;d$S{SzDEr;Gd7+kA)So#W6W zFvVv&^sgxXmKb^aT%%`tw4QM2$wY_F-|olQgHta@x`(AZu)w9flGkLie-)#Wi(64irLpQ@OV?Jf+^&Or{pBJ|}v;ybZg}jsa8aT?i zEM-m~VJkALZFnDNR_pPvSbnB1Dl=1mEiqHe|1wicXPv26zeVY&cTrmNc@*CdMrp-c zQMxQCN*|n$(qpHibkNBt{dhb|7y8W9u%a_{dEuG5!+WNF$}&^qK1Jz;v?wj`I7+kL ziPAn-qjWLcn^RGGJ~2v5AC1ysN1`ocOX7@jv@M)BvYD6MlX zN{d{?18(}UD4l&cO1B+~;?Lw!TCl)OZJKMQHu0FLshMW#p$}17BQ;91J&V#!$z+H7 z4(ExV2`1BlC|$Rgdo&uqS=nc*7u?tHFmI!@9)1tt#$S)pa_6En7H%Ni#Qk{hiPGi? zQF?nH~UO5q^*W=yjB~NQu&~c;1681pDYrl+vdftx(afLFMdvy^LM|L7PY0 zb;D4*j)<^pH`q&E?YgUtU3)gO^ZlG%Z`QS|qlR7Y!@V)v^;T)SekcX&W9NHUyMF3x z*R#Ft`ns20Lp$1aSqr-k`Q5ICf3s^2e|#$2wPsnnZlt~)TtZ2nm9T5{AiFLIwd=^9 zc5U0mt`Az-^EdAp{<)uleTxLxo4YS%)=>{<-J9dJM38d>Qz z$gZUt+SMODz;lfsz0g|%+$!qx6}4+f5xYJ`Ct(|Px9eDXtbpp`+kp98V?H&j*|iOx zlkrS|s{wb&WY<3n+tq@6$$JIQEUoQ&08dwQyS}Z@tSokYNS{NQ>t5K^WPM52SlE_u z!_YeBYU^Ovg82Q;Y^O7y_hj8z-L9D`*|j_~`UCb3>^gjRlXWC~AWH9K*NMTLC7xfH z(JVY`)w1(h2fO}4ubViR`LG-4cNe)E;GF?iojuNVWCm?HKeF}+y5WpXU+?FBvrV6_w&|!Co0j?8rc1`! z^j5e{ZGCK7Gt8z@=%U-E_g~vI@|jIv+_kCWv`ugCu<`pat!oJ<*-o9yWe{VABsU({9-`Bgv-6j@Y#PMw|YIXX7O{9T8>I_BNa58er3^)W>(T zX2FgzK7Bfpp*_8HuxW9wC&2DF z2e;G4_f$4*vBjo?7u)!qp-qe8Id7Ov1DT5zuaj_>gKb(04QyxQXL2^p$$V-spAwsF z`jQ!)cCgP>nDOjG-qNte@%@2Uc04b`C9}^a&g3KWS-ah)li}P>n@*ZW#wj*UAn&*c zd|)TQ&W8O5)`xW`Dtp|f)A!o6KUodt^8wEd(`}j$w%KUj4Yp}@*u|VnrY_8#8P!6w zxGtG!)BpBxE_^MN|Bv&YL&m>s`fi*}_l+b!XH%>kIP4Bns0>OE)t>TJR>yteYnIyg#&$&<-slG|`CAtc@bHE}l>F zN9fhU5xNl1MVXn$>u@dbAY6Cd4%gbSWzhuK)AYEbS%lv7kI-9|2yKpUy`mA?J#U0I z&Jm$in9;iD;d+hP42J!9BV4n>j(|PWHiFOa&|l36jjuvheAnfR(B(OqcQ*F;60TX8 zZyx5`489M1Te4rE_Z%9bwSSAy{djJx7@>LbtOFNApEa^XXtmE|gdOrIT&vy>*9|ah zk)6HTH;K?#&a*=G2yMZ9-s1U(SA;%?t?3Te-pnbD%&CvVwH^F*^cV%FglqT45!wuP zN7V>j?-RjiA0zZMeI8{-1=7R$c^31w=962(dh)nJE^{l99VTBt&(zJtw~pLwAT?hrmZ8lrhn?pGl?<6($4 zWlpc@_aMAM_LfN@IvZ|3eU{-|^1f zqaCnGoaZa@Ud6L5=Tht~o}6nCzG-}2xEButA@9?>g1+))w1fes#ZNx$*NM`s)OfS)$1>-22Nsc@*c#qV>hdIz^@bR zp^jE<-qNaTf3s?;#j34gJCS)B?9&-mZ8F8G4{cVB!Lu&?4eDUkT3xOBg{(X9d>?n%(RJB%fLEdi!iTSWH+%UFrPb|X8?JthFSGLW-|-Vs(79v zZ&}!Tunjodcyez?&+0HE_SzC@)tbYsdWAl(G27am$co<`vZkWM=2rcK^GRgRb>Lq$ zwCY)|E3@B*!9n`AXOKp<3)1OLg7l?-kp59ANb8w{G{rYa7nBLo8Kr`BPQf5;k~2sH zGMa1VkImKlO>>@`GBq#57K@4c%MB;PkwK% zJ3lnn+OUJW2C0AZAnl1~YCUSJ25DO9AT0=cr$mtELiLIU>7RLmG;5Y1ee83 zAT5n2A8yr1dMklfVRX1~kRHz)q&vymGw;J-pn7Q@0d{~ z*eHCbWDe5N%xdMA=6aN#18W88x@tlEb1O*aavlZA8RZ?M9mqKq_A-0ahi&E&q?uuJ z(DOI=1k|PmJ(9O1em|H^!9qb=1g~o_A5jo{eDn&^j3;jcNh9xI%#1N(X56zXEgt+~#2cdbw|aI=ckujMf4A z1?G6u03BllXgvDXD1g6*5THk^1n6Jo0yGwO*7yJ|6A_?|`UPl-UICh94bU2R*2U{W zV1Vvw62R9^fWEIEpsOtbnqE0T$Kl%qcHD>neba~i;r{6yptD;AXj_;lJU_Eu`z=5n zcpfEhyXpaYsZxNJ$JgQ;ppW_o=vTNZWF1JK!<+xlBLLJ(1&=IVC@C+sI&Z+_Yd5cVC1GE!t&YskF4AAxT zIE^0f!1u%_1)p}X-%wlFJv9S#2$_44xoLUkV`kQ!=%FpMf$0U?pNs{ln~Vn64$u~` z6^f(#Mz7n9nafWrscnr^CoX1#H2u}~z3^Mn_w>i20#oLbh zah69p%K@C>KIUWKlSJL18f0LP8*qowH=YgQ*W8&R(#yfdU=<>4$M_c7rFg4_h)~to#wAk z9{Q{6hQEHe>-}-@z+&5{dGg8TKeRzznWh9>)@yU+VGyg zx-a{y70-#M{q@c%f2|1D3(sXo{Pq3;e{F&9njQXn>XW}NdFHQK9{Z~&+=y%bTAJED zWLyaM1+Ug8{IwXXFP>HR`>WS(e;vIIj+|z=Gyl&s>Op-t>*=HZe3sZ> zg+7nqxsSXidOf+-UxV?SgWsu({(6s@yoc>gzDd+pJLs=};57$@!kynoj|u)djJ!j( zFxN-i-=d!W`jK@As|S6BQ&*GvUGVqNEv_?omQLP#u&wdUb=P0Vavo)w-!;Eun_t#mR$5*ntn9CjdI>r9YV8&6GLcZlcdHe>m9_GF> zdH`!dg|AT0OmooV-*_H|{m%Pb%;O*)N7>^YoT58ipXA*Mo)2Ta`_WI|r}=5z6F>F6 z<);$ox|R@ z&~vV9vBzlimTM=^D#K>LPB`qR^~w1dt~!0)!Y7QqW2iAvvzB!ROdPUvoq|5a_~}+M z7sazTIae|NoOs@Z4WMoc@2XN00{@EoFQ@>|KXAVS_2Pa~Jzrg1!&l2!^yTMUzFM=O zucqYj)!{jP)iax~j>+t+6EgYg^B+F?H_GYu(S6^2)b!Oyi`4X0j|#s0+}&5Nn|w9Q z+gAs}-OK8$LTyO84?pkq(HiJE*LC2`Uwrh;M<1P0)mImk^3^tQtMmHm4Y;LQe6=#% zt8YGrT)z?0nyNs{e@U-Uk)dz5EVfM0TssGzMkh%n9NB{DC${Qaw;Htg!Q4jJ; z0pMio- zCTmI99Aq2H`xbB|cwYzggjtV1^1L@X$aPuPw+~I~xMSjXK_)$N(xeUdn{@aNlO}IA zX~B&qeZ9e?TaYW>q_sDhG#}3|u>Qk+{C$&Fzi!f@7fhP_m`UgEHtBfYAEdthI+Mn& zG3nT~CatsHq*bXuf*SH}FYo?NhQGv~r%amjfJp~$H|f9B&7!`b%cQ!>q?cG1vBzEk$)hO$R%o^@f5Tdc!*H~yMQ z2c9$O^TQ@xxs7MMs}N)2XNKIznzT6A2U+8|UqgKunzMp=aKAdqq@&4OoIa<}$8Oli zcs={qr0W)&)Vai@MVFei346@uc>$Ox)~DR>Ka0n4lQ!LD(zo=poIdix9a(77lTMQ^ zS!B}f@PW%rx&i+nlpj6dUQd|R`>;u?aV~e5T}Pf5gPXg+#P8=!S`q#^{0R0Nihp;Q zF6a!`nU0wFT@-t6G3iV^Q+WQEtP#{dg$;!r#dZ6i-thNeDxr5=&);w2_dzC|$9Yd- zemn4;$@Bj3;b;Ro4>z1=ondC7*XVCNhJW$ai66Z6?JI9hd*rQMu6gUEv);Nd(ObQb zdTT3`gl-@6*56Kf^H~LNzBlmJGZ($}8Emt6-kR~!TW>w^R?8J{en;l5%cxB~?5!^j zQO8=7z0ULeDEoFj@2xpuYku?AudluJkLTW6E7@CzUSdD$3Lo*-I|sdWAld}~4DXXC zyfqi>64*2N5BSVG@}5oc)-kueHH3GG)cfG|?0~l(+v}~rvqrF2mE+!OKTZBK-g+AK zC-28JZ_V`7TUTE9))w^iWe?f+voG9Qt|#sD)_!|$QKy)`%dHTL>UmKge}jDKg;5^fNA>%h*S{}-@F@Eg6;Tg$>aP*vE{ zuuj;;^gRUssVMXb=kVB@pRaoJnGbJ$!n;R!-GiBqcA(j0SV!)M^j#P}lHT{B-4DFg zjc51`Z=JyWw!pTeW(~FPVSef{Iez+fp_iZR&pEb$Jq=gtmbW&!>a7i#u?PK}VJ?~B zA5%Y)^*AcZvly5nFp6%W!s9*k*FQY8{5TKY9qFNW2YKj>P!FBb)k9Zy^3abRJ#;Hd z;(8g+YWDI_a~}__Fu+4=PxR1A@SEYEkMhu>gFW<4h=(qxZYuRbaD7>;cJa_H-97Xr z@BZ!Uq1o|BnZ%xO`|;fm+Xl8O?~`DEhZzNXhBXz|(!)b9hI#0Jc*awA6yN=@(dZbN z%MbL>Yu!BbHSE^TWVd?gW%yh_?_sC+_RtUgJ+uYv0hlN>808u1p^-e_hesnc9L68^ zJF3DyX|RfKcs~zy;c>*~p&{gLO5U48>6bq4_w>+3bVZ3L*Ui6zr z#y~V5KW|nK`aRC`=CJkfUd%Z@CToARgkJLu@z4^?zYy>G;PILI+9(;%y{so-{b8q| z!kn{<*?t`1p$&(7=tyR_0yaN;PUC%UbdVmWvd=r#N7G&c-n&%bW>GIYiLz+-qHb;iE%bOwh@?}T}dmZ?cF1b+<9_R7Q zk6!`U|6nJ5NSAl?c?9-3T!dGKWGav$1yO(Ys`imw==G~~Nru@A{}G){OP8Fm1L*Y$ z^LY8R1WHX!@ z@0n!UO5PA=T^oKS%m>to9DUxVi-~?teovQra8cCfFO(qz3TDUz`u&$Vc4sC-;ZIUu zf~?iq!wVf_zx=$Lg?Bf&*YLOS{f*4E=%YIQ6#JSkG1R2P=cj%nd?U1oy;gBQhrY|= zBRXi=M3)R-IiAq)A@h z4`JPhmT)~9UEq2Y>+?*nWL3^r^65>QB&4NDb^LPvNRua7UdasJZ9;{)zk>{}7qH%h zEzk7=VrW2xc&l`dYudlFqqa z$q+n?^L#AzfpGcAbp@q!-H1I;;xUQr``PC)wdJ|bDDX-ylY1As$jp#@PLr=>$ud7#<}XT?UNOnCKQ3ANu1S`2o0BEau4I|AFIhGoPL?Cb zlBMeLWchI-S+1T+mX~LerO$Bh`nJiP+Cd`bvMkt}EbR{^OWC8z zQUWFdrtIltX#itk=n8alrnHUgJCAe6kdQ{eV}q zP06xqYqE6TnJjha@%KIK3)2@i8QmdsZ}fpZw!x>8c|uaM{7wD9?bN~_;yMv#4)vE< z&)_|P>j@}``%JFg1s4Lua^fxLAyOP|m zIfGj0HM405a}NDrMvJH^LdFJgNjzK6ULSZqmh~~uIx({xtg|@><=GeNZ*X6ieS6|x z9oEFN)4X58{+&3pV0bTTck*s7HHYXw`sd1CrOA>972^I3?|){GhO-|?mW=($Qh{|k zd%WcN{wzsSCP$K7%#$S93na;jB3%0UWwunioz1bw|Al>ps_U*r5xH{ zFHyz>B+5Sm-!f57cSw|&u8A@dE)}LHwPpGw%5GGsZ=&4lmne(sB+7CBL>baBQBs;F zO8XXx@&eDWE{S4=yN=HrYQw3$#@ebs9s?7l5*m)*xVnkbnXHF?Pn0FC6XjhnS;@Es zk9WvCAW=S`-EhTW{y|ZL66F-^mPU!P+(?wYLA-C5D0j)5pNvDOy-1zM;6$lU?Ru2R zeOc}|pyF_c@qC9~z?!-zN*(%ZM!sQqU7%)Vc%rmq{RX=Z?L_st@5g+qNTQq}@4PmN z@&oP-Dnx&iVa^WWdT63FV%;5)DDz>SpfuFI31`EZbZC_*4>^-0eBaV*3p^)MTMjN2 z?mXB1sn5W(E$d*EMLEl$M48EKKXgizip=&4dGp~}hWgv+6W95v4W!o?bd`N0StlkPW^Gl3p%B&R0y3Z|Ea>Tp>Z4l~0h(<^)-sJwd{=!Q@Jivw0FEC4Yi6Et(*w zeG;S*+-~aUSLV7(f*faERw+Sd@vH)SoXL?OukbC7=lWj~WT%O`5((0#Y=WGl<{y~r z)e_`yl(lMtWN?2ECHS!yS(oD5&MQHN;<*6NDX?34SDpH^)Mu~pKNaP^8A^pMPlnxi zwuJvXcY?hB$WWLXQ9y5zz zSPOL*U|*qXa5K4I12-M+2Yo-!oFHj%dzh6wUxLhIe*5UP6`A``UkA?|=q=Z+(QCLi zXanqI*fz|oJehM8W_HCBesR6qxK)pkxQ+3pje*JSMNJL(~4)~gI>V#{VYl~-mnr0Rz#buwi@JI+U+}vFx0v-$a(9A_Ciic!wL;>hF|3Pz#|(;>%kInwo6%wFFgy`^J8Qc-EHrpM+~bZ#TNcOJO|i zuy5)4EwkP|BwlXA@21uRW&rsb!~8ZfUe40f^kMNbj+$AW<0VH|W{!7U?|50jS=Hm5 zy8g_B)}v2&^rb#O^^u%K7xvG|Y!-ClY+-+9?hhLUdx3c`pspzOS=pyA^$)3C!`Dtr z-uK`<2axeonela4`gGNW~ztP744zYcNIpkthLN9DNQ#Iw%4OQ9zC z_c-a=AWpLV7AI$nIO)+mP71V&lRItWq?k2MY@J{_$4Mzv4E|9&-nWU9gRnbb$2X3X zH?SvRpTTZq-}UV8qIN8dtxKF-XFbVY(|JCX_ZwQo$$m0kBJ1gZIOzntv{{_&YZ)ir zy#E)^sc?SSONoBHTQ< zAhJ$?+W^xTx$v#nFiu`@9fI$KpSkGkD4K(Q@N6$$k?fs8&e8OkmpKh#Z4BG9QJh?1 z-Ob+Ls5?aOZDHzj--G-0c!j|^@t9CAP72rl-}QdD?x;NPrZkC@WY$DHYLag+>%TmE zh=+-@jE7lXlRVX#5BG1`rxF?J!>^;}DC{lzFAdXxKEC30vTmH%{o`b7jW}6ZB~G$b zA~(YV7p@HWl{DT&LWEdjfNxH3RYY!RkE%Quzp30t6oN{o2Q<~#sm=4Y z+^3VV5EJYb zpLX~(C*Lf%eDpT!FQ<%!TMU;2zxr^i;5_>~WlnFW_)=e*IlP4#4O14T0`p(NOpd~J zC+DHD^hr+NAx?=K;FR@npL;oFa)W5u8yGD+f}&-AaI^$;jFuK%qUBHYx>K~QwMI+V zcG1$Xb+j~S5iLp0qGi?Z(J~3$fm^Q8@_U=D0f0l}tpc>Kg6{bBsZeWi{bRjk@oc}9`3B^e*UCBs?q8Brx#3cyS-N6QW0XjuvS5hcJT z)8o&x_(aZRa$ac|Et%@DH{6WM(UMv&T0%-k%O549;?nf0f~Qk=uj z9`oT@(mz_dlQSN_8f7>mxNAk3ttna_G1Kkz*A4C|Szj^JBJ}wg&s%uD!E+Lx+wnX@ zPG@oET_jq93-N4#T~0;V1eyO9%7fjN7!W*?{81COKzA~Q|vMwuKNUPY<6+LO``tnNV^=Owlh-p$P6FF zyZY>33GN1pWsk39m^Su*UL9e&54X$Wp?IOw=nP7v<}3TpfjLIjS$OUtLlu<9-p9z5 zpPn8KCjTJ2WJ0wD+GQi^2h-v&yJVkE-Bi0|gN-J~GctBzZ!5Ju`*YpbE*X9764Ki) zg-{XnVuoE>&BPx*72lEY3;wW6PI~(35d`BMW|wun@ajqS9(Kvq9WRvlw_V1~{vW>s z{u??@pV!E`i!=D?5ejF5+1|}AH#^&9YA3stN8xbcbL?_|7CadWGT+biSqwIktV=@e zG8XPaSG!!M{-~9HJJ{u3d%IMKCIj9ZqU`buo-XFLklFX9S5Mdg*l*<9(8VrKI@+Z& z?22}r0s4s7NOWbIU8*rR3v(~RoT@WhGwi>xd+_amZw2yhg1v?3YIMD5q%`u0lm?X} zWk>Busr*}{Y-}1S)r?446c{Pq+;6QHDP?L!%8Htive-XT&ee^SA_0-Iy-cJGtOyIY zhq}2DX|SAcD=ZHo_7qOL&*WKB|%O7VUy_!8PD{ zr|Oa7UoBE{qy4BUd|}viyw5?dePn#t0z(=rt!+QuS&7RMAmp+uaj*OI>5xnmgDUZoNgjppr zlN;=vNUa}!gIJfNWB9hhw*~L&4vUnsXks|$&^uDX>A4QG{+*t0(pMMu_n>wS+RF7+ z7!T&KwMwL{8Wbsq2Xl60pBoY>yGUxe~A|(J7XP@QF_z^RjPe1LM<&Mr_ z(ye=#oa+-NyTZd{%*ZgwG%ieTj}Mdl6T+l5>um0)jt!GgTbL~1{@*{tr zU;2hg$Dv^|ZgiL&qHY)OJHt8t2$MC`r=o&f&*0g%DPfW}Ele)W4wKhI!o(OBCIYj1 za+oY+&rcJ>^%$5a9FRoVe*0cPV>T~q%%zZ{wGXw&IuDcHCJFe;8$-9{jzo? z?_hhF)R`W}_jO^?6qf&4v9wnC%N%oxRKlb@ZRwsUU*mo~E;}?>9QG+18$7c2!<2FoE=uuNIQx;9wq#Rp5t zt-5Qk z@IYs|F9H{WU#=6uQtM2x^t~7?M=#S8@2~6$7A4OEYWf@rmiqgG#d}w_GrAfqHE#w>D1E;oe_izKSg-`JuNT=W!Bp8FEN1rVbt+ii;FWqkSnAvk zmUhX(()nSqoP?Q6_QNP1RXm9=>|hiRw~G7YaCPzVf|>a^SSmaVmWwZg#Gr3RhRA&4N|98Q%{4?i-SJo@RGT|CCr~iZ0eq_Ji?2(V_ zwLD)6Hv+HgZ#m~LFd4zJI7>UJx;#+6{}(8K#RW=(6@hYcWuWw38z{OlP}Xe=lm`ic z^5kHkbWNo0OrW$+3X~Ez0_EE6Kyk$cN_BJ!wxBCe7Q= zIGDpJf%0E!pd5@1lyhYMvL;Y=z7|N6yom1LepLa^jbLU!Wx5SN}qw+{KS?h^4{PK*OlH1o(`0i=K|&On)ONtwoYoS)|NLiyU`Zj zi;Q__k^CtZshw((V=FB3Io2Yx{YeAFs*z-Xi7R zlG9<4|7Kfc#XlCA^RGoF!PZ`9kwJScGM%i$$U6R>MRLHMgB$&k{GTk6nbkDYB9Egj z@)9=hQj7GX*Ivw^&>@Q~!!zqmi#&O35%VjHl!jUG)gq0*I1hndrirfqo#akogZV7DV%EwdThGGA|LP^d)gxN@H|PMb(v=&`rC}pC+ZjH zswrhWYf26@g!8De1!lKJ4&pb0tnMU>*znB2v*Ty3Gu4!jaAm2Vkh`Wl%2QLGp>E5} zGI_CC)+{i~6^B_qMVlp(HD#e$sxLRoUmMNRb)Q)rC(WXl&9dm0S>o=QrEIcUoYbaH zH%q~3X6ZK7EJOY>OOsh<`OaQNR+weQ7PDMBWR`E|%yRR(S@OZvxo?*CDBlF$k21@Q z(Pmj{Gs~$-W@$8oJ@D%<|gG&`oAp ze9$Z_&zfZ;>{oo9FuBn%uK#OqmgsKGGR!Q$4mHbFW;Smgx#G++f?g{eGs}VtX8D6T zMcgqR&>SB_S z{Y}z$v`ISuWs>)EsrlC=7hvzNG)Z%pNjw^v-{bfW!`HFM zB;GM5;j`JCze$c3F^S)=CMoY@lHKs-@UPy)BvGABQk#C8jWtPE*aNdol4GGs(%?to zAF$XY(RoZ#FuzIW6gJ5;vq`4cH%a9nlYFM%$*`?q6YVAmnPrl>%u3{rv6V)xQ=5#$!?NAelf|7k|rt4tfHB7+hCJ) z3+0^g-8jl5E&pH^6HHQIBE9@v$D3p(&(`N;hJ{S>C%xXRX_6v_N&55k@uHVWt`0KE z_2DL|#;nJ4R=0kN>&NVO4~>4}DR~}y%CLu?axvLcHs14;I}bdie5$ATy!GTWT0Hmg zlHgywq*YNb>Eh!hk!8K)V|g#xc+*p^UiFliE1pvBvZrKW{eHt!>a$m!G*8L)mFw(Y z;tBh_h?i_F?IRRc!{~Rmz+X9_jyXz-Ja5YkEd)t;3=*|_;a4}{H~{5gB|wKQ&KW}N$%WUlBIx` zR3o={aW8p~j;;5U!y7!M{T5F-xyw_k9`Tf8r#)ra9ZzZW%v0`u@RSihJY`-EFWG{3 z&-`AJtB{u{${p(|PvbnLqRUg}!*4z6DT6O~O8(nqcuGI-JY@{?Dw)MgTEo}!^b)I= zmn=s^|M8UJOFiZ23QxJb$x||5&(rf?%xgAhFzdCa^rz>HbaG{)p8QSOXFAFr?I}H- z^taqomTu;Hzo-0m(o-5=^OXCX)k=EK$61Bbcg82q^NXkKK^vIAWrnA$nCB^L$UZ&Z zQ=*xfFKiXgYAHPzy@W;A;&m0f&E7RzIKLUpIwccuM8~w=u7g+wg1XHYU_{8w+Z?jiL?Q#=WL)V{u!z zu{6YOR2=R$T1<8u`{uZfsY~6)0GHdaY;YTMYq*W(mE6YIif*HLIk&N@tlJ3YeqSB8 z@lD-^xwG4-IoNIdI>Buu{p~j9FLoPGm%ELuYuv_Wwk!edi_U%Xg7`HKKn%met-)(eW<~C~lM=lSy@h&~XXztE1-e)FTF1L|4pWBEe zhm-7QgWN`?UT)*kFt>3Z?>aNxM)tXGk4NA+f*hmCvpvH&yFbI|mx%w>3?uw~hS4V@!+4#? zZS2H5E8bn0c}g3%@u-X2_^YSec-Gr(EF0i9j>cpdT{mPHHxe?8N#`?+*kri03}aDN zw~>#z%`eZqYPpRLfo@}DGq;i2+HKtG;5Gufu}5@Nzt22x{cy~LSVYIxPVLW4Ysoyh< zGX>R=>yc`d`j%o8`jlb}LZ?2Y7|YO~Us8;M>`~h*)o>POpE9Y& z!Ro2T{)VYW@88+GO{(#0=Tu|(>lEX}lN6&ZYKUs5q!@9}Q;Z59Q;eeIn2P_|VyQ-Q zg;XQVKh1KF#H#0Ni~|} zPcmqimZ*se zDr)QvdpCCd9@g4t-+RBi&v*Cve)rzLmcQR~=A2`W=NZo!Gka}!Tn95S`=Vu@P|d1O zs6yQ*R2H*aI3E2@&7-iyJn{uUU~L|K-kL{+GxO*zynnitM^j#b|1*#37I{J^%RZqn z^j=;4gv^8TsOzviikpx}5#Zm2hhyvWXw=?3>T)8FuEDzu^Ecz|IOY~v;tBawME`iV z7y3Nf9g|0Cqw=T<>aQ?EXC*wK_UD6nRODE^GKbFB$)Qo29NH0*L)Dt)kR;^L1u2KTJLXVqj~x2LkV6B9=a5hw zF<=z$H8g**t(0+9e^=z0!%b`();%+70Mx9fgGwW z%b`H*n-$5SiA8fLrc@5?hK^UY9Qpzu?dzi!_@*{Fbgf$s-9+upU$g1Jhir=XWYf7% z+0^+N&JEIEVaN=1`~h=nuZ@WM`A_#cUdUHk*r8g#d2sdYR3lV(4^)$^e!xif+KS%>PR;EAI+wa!`XEFST@bQlueJI zciWv!-p{jXAbRQr@0Y-B5|l$-TI5jac9=c5m$zrrJ!>}oz6<}2U3xY-k7v_I^wH#QHksaMQ+G~;vz^ZDveWp( zb}D<`PUY^}sXVwpN7>0R(N3kOL3fs&S|-@(Aas(G?eqy+4;R_#(keTd!SA-;PK_LP z!vAw2Ma3b1h@A?KKrQHej<-`WtZgRYeu|wW=!Kw{VT-{_L*8CHEr9QD*X<;WveRHd zHo#63fYmW}I$^NWbZE93X{WZBMah|VYO%mhy}(_t$4)V)?6e!)iQUm}S36zqXs6{m zJFU>eTW>oh^|Mp6fp$6uz87jY1NYi`c^v(LNl-~u~X{rcKU%?pTMm4q2CW$JH@rKQ_~i9x{7^mhW?<@6B?TSsE@kC z66`c}rJc4~?Nsp?db^1E-L_NJCU&|L0^LwMt;Jpk4P|RP1w`0sGv?qs#!mj2ce%B8 zNcjJ_LS4vpX&+(vEe^ak8q%ywCEJb~*%Z zX>gl0w9?BaR=Uru)UBhH&h)g>r)VoZi?h=7kyZ+ew^G1ZD?J=%rA?Eq%Wt#!8wlRyx$nO1@E6ijJ|8eV~;-0LoY^bslP^0%MRr%}VJD@DpY> zS?MckR%&6Tdf-;;Y^Ct-R;n9mrRC5V4{*JeGWuF66J9zFv(ggOUJmZGjaC}-o0S;2 zO2R(OO2O@|R5;v9Ii0K&3a!;xKXkLwfPPjQJH$#COji1RzLoZYJFlsg7B#oh?pE-q zwNj91r8m&(%B}PTdWE4kLT9C#eXzp!!YP=;LMw^rcR9F)fk4b`I(j#v_XRlj0`@~w z4*qx4&W7)UBhbT4E1g|pr7Byk^tln{22Q31YXJOXUpojn^{tc#er<5aM_8#JX19I3 zl|Ck6-m5U@?N)l+z)Dxa+fdI+?Saj8tn{k3l};db0Ni)2tu!&xO0Ust82om@oYt&E z54)^1A@Hx4!CJeDqbF6K!78_(CZLEb3 z;~a}#PA6Ds>{1I=-UeQO3ssM{kO4V9gDvzO>+vWHsSw+NwKLE@#zMv6>Ei?oEt_MZ zwJXt|)k2k!djK_8K|>83hxhgH(GPySds?V>Z_Eg>WYq13e)r8quWKyy%x0kn@N)}X zEoOBU`PqFeRIr50z=>YxEw<1C%*naWLg(T0s=-3R$Z6XHxjGAN z!0apI-4=JX&_ZY>V*b$+Ei@YM({>eRy4^w@Gc4qWpJ%8SgS=J9uMD1d2l(o2p|9O7 z)CIbGFy||HUwx{D-lXBJcUj1B#6rW+PkZnxfw#Jyh3W$t;2weJ-*ML(JuS!Ebij;8 zEVt0B%@%5yj$V&hXybk}y*Xf}W=G95>ZF-IpEpyH%Vw%|%}lkgo2diVE?3R8C&x_X z5kH)3rZRWU^z?<90`{4y{UI}5K4B(y&P-=6nQ8PD0QtX=zxRTf=0or4Su>fi4#_st z@tbCv2kvq38XYmyiBo2p_G)F|56i_ZT^&z+L|uy+W%1@~WdoJUq{Hm?`48 zndH#?2u}yl(<#6=!%X+U?Q_vg57BSk_hu>yor+E~4Tpw+I`4pLs1b|&=ex{wa;KT@ z>^2jJ&Opqv7Up&Ip_#6F%(N6b7g19OjuNxJ1fG76nVxNj-ZnF3VfDk=cGygA_}hNn zOq-vXY5zAfMWL<<^H>1h#Dn13F!!xyn!4FcZMUEYa9gLt4|-^fSq#5prbp=Y_AfK7 z25%2)CBWli_?rTrA9#fUJGfgBs{p?}(Nn|~%=M9(zN1%%&s@3%-bCn*x56iK-)=OM z5%{tRvq8-%xND7?qc9US-nBdYe)wc2Rl&LBoo%Al7fdwhvWYg_Fp>S9iQYajQS}!l z@_%lkg4p+YV4@bcOfCiCpdq; zWTN_KO|&P=L?Unud&@NwHGgBGi8%KLzYB84qV6W>24XD@AKqt7v_8{BclVemey52V z*-R9C*hB*^nkfCLiTqI0iX1h#?~#8V^%G8+NOi(Qv-g^4*m@H^TxFu=fIZDb8PG{| zm}qI9iIU)jP9^`wy#9Yq96MF8M zZlbAMOwf3~o)--GM&#psxn+O|%_-P2Xpt_TaddnW+41 z^oCsLP!mPRnrN@lL>=QzG$#@LuEsnwpnu&&w$JDny{4^82L#|Ms zO-|S2YT`8+y&33nf`J;&GSJW&21=i9AU)PaSf@=jP~2n#)tY3WlyL@H zJJvv7u|7rY=^O*SMqG~bz6k~@hJ8uE7kg=pfii{}$Y+p&mP8q7Ssw$<=?#s329hCW z!Tr}!25L3bKy5}E$U53UUO>k<)QvIF^Z& zHyAOaff@npz^T#GK;c~tbh@>HiZ(G&!TPA9G0^g01D*NJK!w^u4<6*7BlK*paSS+6*!Jgs0BZA_!|pt zSv>=tFK?jZas%D3>Ag)B?yaWuTe3n~l6@;9f(mZUW}t&_GT64RpJ*fi|JmE9AcZ7Dc|FqA2on z6gBt88T(83LO(iaC!$sf)ENcNW^iTTWg*_An1LQZ|1)MTVg3#9-Vy!6 zX+UH+wTcMG@0Y^qLhEqa!^6oXhSMhC4fadhgwt=~;Z&nzIQ`ItQ-dDi#QTO*p|0Vi z0%vAeI3-H}@}IQ~r+&@D>2p)$Gzq7MSU3I_PA3t&*eaafv2Z#It^44Ofp$-v=LzAo zAG{05_YMuGkwM|)qYkH!b;GGPFcTOP5KgNbz#H_2pjJGzHh|L^x+TC{pb4j+$Qi5( zr^^+>$yp|x0uEd73kj#_VDtu#MHx=tkz1v3IMwk9 zr|h3$G~65g$)HgJ{wkq%W8{L9><>+NlCbW8ZbYeYno%H}dVdb1O>e>|>17zr#+v*o zjBZ220{&R|yN-O{8sT)OaySKpR{-&js1=92(l5ej_royCxF1H&$6@ps8qr_FXgU0J zuZf&0=nXyL=aJIVLgAG0BaE7Q!YBp2OW=&T8Afy6VbttS7`=WHMu%`$(J!3vGeOC} zC}skGGW1j#9<$y9&>w?(iMPV2=5^!(lWvF6w5MUT4&3G7-u4Zrg5Sc(gZ#Kx@boN< z4(EkYxd&lnM4cqm)7%InH&z!sl!fL2c$TAHaIsJ-UnG=l1w-lVj}UtJ5qJ|qw_b&i z&)X2X`zeI>dxuizqM?*gHk7Q@Lg`eUQ2H$hdwD38m4#9npHO=GErhCjLTKo_5NZHy z{18G%zJ%aE8=*84`cyiU4p#uGhSD-sD4hp)Zh=sm=M_py)Ghiogkr$I3D{Bd9?q3; z*A>3(g+u9c@laZ;2&Jv%LdgWZnczNzuamz*C<**IpP}(RghqiE3%(w;kHgb)+{GcU zrf(>@{6Z-ie#c|q75ue`M*<9a67r&;G3KpeOou{MO) zudhR>>f;a^aw~*J=Y~-0DzSgtj_DXyw@uYI^~mu7=Q3%u|H7Qg{3+k189zq8AXa2id(MPVwM!wiHKgd z@u5l@U{q3%0ZNL|E6LDJNrxho)Tg_Wbl{o5TNID|G$pM}R+4L>k`Av_(&aHqdNK^W zAxf$b4;`YEBtyQYuafp*{}MdMC?)MRDJd}l@p(#WutZ5iS19QOe4HMkq%Poh2j3Uo zPsAuG1N$q8*+(j=_5}Ds%?61|N-`^H-U9eu1mCDTceIjh(Ef(J{QY>Wx5g<+1Lw*dj8&6VF1)P6CFAei(0t7&R>MYCw{2|EcjoclWSq@ML^?rx1jnFHJ8rA10 zsUQ03fj9VpnaHqz1a3#nX&mw|<2{bzJtBe2n1>v3Gd%o)#|GebLH^D8N}2^|QFj@< z)I^OsaZ0)tt)%IgeTDu?s%KErU~nIUyBOS0c!M9{KLzJJX7U=mLGbwzyobmQ>a8Re zsib2)pw&xBRVK>udn-8|Op?>F*>Z}VCa0HU5gRI}_5^wQiljYQNuAF`WE$7H7dy1SM#LH;`xKCr{v<@8Ca5+7LUdgF)>H>Y)d^t(Ww!FR@WjoRP z9yyJ-$*IW>%nDkAFxQF5yN7->@OT0<_>Q@~1}`1Fir{s#$|-oaoGt+;cFF0&@0b<5 zJVLMIz?+zY`Qj}vW0v}59=>j$hktT;_~IKL{_vWIyU%<0xq}`)ZoP*$oA2R$r+IkO zF&^$1>ESy^dpJ$XwJsL(P|29$tEnhr8hU(-sd8Mc#4LC=0}H^zasl9o^yK3DBQ& z+r#&{J$w~rwZ!4!zhO?D&~Ij@hY#QH;hJ<0F9@w;(3%27+dTX(c=z^tcwVlDi$K@~ z4=)Pd>?c9$`A0l_8|I%5O&`3?p39g8W^2jua0AwgKo(-P z;H5J>Mc+hSXitas>60F=KMKF#&qdw3$eoD0y_i!)wE;f`{qM!+;OYKcz8STDf-@9# zBY}g^NkH8a;GG0#2{hUP&9E_bv<1=~r(V4vGh)g~pE|Z7jEbF;I~J&Og_H{v@x$e z=q(7mG}K;?yoW$Fa5AuN2X2E`95oj7fhKBJM6Zi6s|dtO!fOI@mm%jla;IZI9Xi*6 zV?aT~UZG|oa37&&0@hOCxsa#B-4xXK!MP>)J#gNL_yX_>W6kd;8e9u%g`j2xa^E1g z58`VPPtCsrCLtb*xDWWpfrh{|&;H% zje&*3(s=Z!G+uo?awewnzQEWCY5XqEH=!E>1Y^Gzu@a^<9yJB`Q`30UY2Xh^<6Q=) z@kc|`c*T)vyb^Rqj7Z~ifMLVa_z|Eh;%^YoicjO-(CGyImC)MNFO6T0O5;BUrtw9w zX}pdxjmw}HKO~JOV!s5sKd|*Y zTKM@0jdSp42U5GG;XTrL7!c4Wjo(M@7U<&{_ExO-!QTx}!x8VDUmu8p#wg%Y*EBv5 zHPgXejJh+yxq z)oRJSXZ2)$2DyRQ&#jWob%-}YZUy8WEt|}Dl}YC770J9;>13W!Dw)3p7|z+ae*oP> zfH(G=kSikYjlAW^NiUhq`<6)NZGn=2yjU_HC{N~NizoBJ$XSWp{aB~sJO{bevA!sh z%p;1z6VMs^>AJDG39YJ%Q=-0kv7=6$@-E1&>;fp}mIFbF6GjQy0vMxbiZI=e|wiN8TmzaUYX-05JJ|5+8~6 zpeKpzpupjYTa z5*MNK1y~BLBhY#d$f1#SJ&EgXq5j<@o&+5WbT&VQ4m5(HF$WqJ_(+AuDe!xOUk}_V z@KC#vk*fqF-`dW|<06c_XQYuI(i{1#-bTI@>zAHJ{;-FUm*{HbpL9mvrn8a%>WI7c zMxGdKlIt-@*xG4hTrjQmAgBcF@=ZiqjCr%qVIkUJ;L z$P0!W`8i+#;Mc*(?*os4ZK&I$rIFV~-PP#1K6p{sFT*|n>jG}%CT8S!MI%3heJL_> zr)1>qTN}AMz{sm&Ub@CcULG3h$XDY&y_Jz?p_f`sjXbD{kzd38W310`UVwPdW=4La zvXNJ;jrr9z^6t=Chx=`qpCQo5Ex>ZbmI40Q$7_syKGx}2I|U);i+l{7NkEd^460d=Sf9Jd5Su zU&r#*o>=|^S|7cP{7wNQ{|LSQ(2Ff>N0WJZQ`Cs#1}s&J_EexZ-|cp zHxRt)ABp>aCLa2o_(?DPh%1@I%Ys+fNqor-;`EsKnODRMpiWif-b9^ez#Z&^QLp?L z;u_Q&qg(N=hJh)egU%5|w{Y&C$$bA89gs)%7Jq=!zhxj0HyP)nA)LntPLkmj$ zpbNdCCjM1O?gfuEkuwJQXTTi-?mF;V?O*=?DIZ0bGU|twFEKrtM??SmSrw0Vs`%Dy74LaX#XYxG zywhV9rxz+dcT>fu zK2Y(Trz*bSrHUuNR`F(URb2J%FP-P$7y7E=!@(VNK*hr{R2)BCfUmj;u3N>w-&65- zPgJ}YYNx+a@!qIk9{iQS2GpPPMa7T)RPhTtRs2f2iVr!i;_KnPHGEgg_l>$M!EFwV z0Dmp&X8`&2uYfOs-)4)7_t~xDPmZYgV26sgxT4~bH&p!7Bj|x&@VSbIgFh2^iXN&% z#|J(-K;z5?%*3kVn^3z9`o4~yhr)NCyDGj7eV@eKLg0TZ5Du-afDH(Jrs9v+srZ&{ zDz4e1;-yi$-USu^nyuou;eGZU^mJdvFF%9^d<=t5Fjg1NU!fZe?pOHs*`?yOFtdrM z{q~HCPXd4Pbrs)r3vqZ@f%(+|e1P9@euJ50q^WqjO)9=;JMQZ*)iy)EO(FJ%1Udl?VJeBl_Jc<4^X=cy^YIA9u<4yj&UYcn2DfW&8$a5rIC+LE{d*octi;Y0%oW8hIOJ zJRRIJdt`ji2^l|hM#d*zlJQu#j4!?~<6TgHE_%oX-wX4c3yoN46aY^IZ<1BUbN0*l zqNAAiNg1zkQN~r*WxO@KzsEbwcqZe8U&{DY@CES4g1>BsjK}Xnee@fUiTA?HenZdm zv*GWWjIW08QsBy-%6NU$t_tp$H!}Vm{AHMPwG0_Iq262g?E}9*@NN@xWIPjfyMddG z&p)!o#bW*r@C3hY&UDM5LoSIWp=pPtOL-2R^CZB8-B{o zGEcafm)*@mGQq{2y1Sc6o!zW_H`MLpX3b*V?9ga8o1NrlTbH_7xixN9WQ&{a+U{mK zKn-a2-R5TZe@BfFH}h%eW>XrwnXZ+am2B^3*Pt^!&do+obThw1H~VA0n-zx6(pAWV zrz>mSY-F07byd1q58zi-c=30$kKoT}=Vq%T-Rymgn|*-?^;Bq~pTqOqOkC_{{gDW{LV^(Zf$8<~0tpn~q+S-E0XwRDp-L@GznbYE^Wz z>(#(jyIB_e*KLkDg~M-8H>)`i>nOa93Hr0ZpX+8jP+Nhz7S!wyzh!H?*$s`GsnGW} z(an~1aI=^mZWcMf%?1y1GvD!URtUALq4tneH@mvR%@R>B0K8@3y$S_dxY-`U9N_se zW)|BIePQbeWUS+D?D7EAh_tcT2pj9u*~Ua*GElv@ja@X@*!qDsRuEcqN5Bto z6DWi_-4RVwKt=3X0~<3nvaxm0@gW=Q+RnzbIvab|-Np{;vG%jENNB{?LjN^v zY++>^ivp(9wy|+48@pN;p6lD#m!>w>MYOS!VVGTK8{6B>#zvr@3g|VxjE%Jg=9adx znPpM4oQ+j2k8?Hj03Sz#P`8PV6%lN#X&W1(PT=;iF%N2g@w2hpz_G$MR$6XjSBjx7 zbbKnl zxQ#7SU{)2;8+tlj5A(+yZeX4dG22#{<9>fLs{~A~V`lL+%xqXyGkaMDXQi2KsAXn} z0cKXOp_%P(WM)21%xp`jnJox5v(5F)EWNgw&8}`{D&$`-Yi0qZpi#oi-^k&1_e3Gpk+3 z%q->2j4GJfQ>ymq_B6mvqIN?8$CS49N=*f`U%NS zVWrNeus@JrGb@D+JC?$R+ffU60!00h!d~Id- z)yha==MSW?9{W>R1lHdVfs6R^6Ntl0zK_r7sW?3Bo2q9ert6s+P))%)NzWPsChR*V z=vmBsJ^Q>;&k{BQJM{SPJv}SBOV3Vi)w8H%Jric@St|0@Ow+SgCOr$AsAu&j>sig2 zdbTY^&%~vAwrs7Qb=jX18?YIG(2pZ1wMS7M4d-igP;+I{TS4- zAzuK>$LiT1(eOC{^BAOOE1-3ExSl0Kw+QkELMsDx4kzi^B>1Wej(wn>HA8N1y`D|% zu4hAgU}in_>{@TkK1$Ed#pv16!Ftv;PR}Mo>osPR25(x#KlefHu6mZ;PS5Iu>)Dww z^p15^2R&P$!)$SXhN&wbV23-!SVY=pSf={ix=62k5lL{e5U0gO~H2 z)hxJ+nzikwW=(piSx%&y)zz!nmp*ECB1+AQ4^p%KL)EPOXf-P_LCrLi)yzFb&90i% zY(sA~^8vQ^RkNY})a)S;*I&(U8`P}mKsB3XRI}Y9)a=w)HLHpIM@>|-`ru@Oe+xRr;KdD3JCT!LqayO( z3_u*~9UvasvBThNl$u43QL|;^)hq4cu2V_vH;hl`j&80OX=er&k=0n`Sp z;PwIc5V%Lt<37w)i~Ny2(H~|$8MD6u9LD|`Viq6|m5BKk+s(!s-tMGk4}dTr0oVY{1LUareTbU10&fR+{ZZp+1iXTCpuL&}wu63{nmOCT z56})sXoJ}TSBI;a8D6KMr`Pb64c>HU9tI}~oQ234(OS*ElA6r~)^jxr044&>5`0+E znLcb3FmaX-o090mmM8hJ2WB4@w7`e0UgX2lm-?`^%YE4Tl{l~RVJm=>D}2~&?m*s zb-tzgum#|$miVv=;BAC{ss**y_^|2VoL%R`{L$8GTMMD6vc@e?@?tP$wDC+3v~ z^Z^oqDL@Cng4s64OuaGR@!+b!?GBC`Tp#EK;I0_353?5mH}bjz_h(~9c*7rf%S$uB zhvzErpI`GebVYC;<>$@uVIPq9VypF8 z;*_mf;^CcH;+ef!;_H1`V)B73@%;WQ(UhJg9@v#725v*%hAi=MT9$YkI&Q3MH)e^U zh)>3SBJu_v$`VH$&Jw>J$r68Jb>sXPTE}*0iLIeucT1L75U7Q{{j-r_x3VjI-@ zn3*N^Jeno819|{k!EcCoMewig$P&Mz&S7v~1FON;L9-QV`rtkbb?P8L1{$4#@dvZS zlDHd#yTUfq0sj+t{?PcbIZO1Q?tR4HqV^`_7|~~Y)S8C9FZ!R1`;s_!Le2u@PX%J2 zkq-V;pvLwr@v=2bw4v_+kMK;^1UGD z0P^n!q8|nBTVk$#;JZAuY9MbGVs|j-nQOAdQdpZ{{*R#12sM&{t=LE5?Y`r_1%4RY zHO#~b?e2(Ahn9YImiQ1*u7wVA{y=OVbQ%Lk5ZjKsCM&YUQ;W02ov8U6YBK1ZMLZav zll!<^0{CEG&5|V+L%cKARya4sXU7O0TaYD|E0ZdAE}bgM09UD0aT(Twh((r96>C>Y z6^m6(6`Ltj#qq#)fK^Ks_f}374Y&`lk}58$m?{oHo&hL>cpJp3BX2~FRMD(T6?fE5 z6&Kb?6;}XfYo&@t#0NsNGVc07t2J_qR8JMX5Z3{9k#iNCmvvJ`xjI!`0>t{KioNTl ziVqQAfZPMna@R~1L!r?$zXlMAvw)ld$hBi_6M&uqQ^j%Mha>h0+z9CZf^Ki{6L3C= zTJczaf_ns`fr7w##P=h|7u;63I|zPboLtz5{pdKWeVT zc_O&6$O!@W8)~V*YlZU`^xqC^0?y?T9|nFoaLa-_5NiW)V-P=vyqkF6U2S#ZjW#-Q zCe}otRG3aY8LksYx7Ug1I_SjH9d%+XP^g_wbRsXA=|oKnooEk1JW?lWyX!=GPn~!S zSkOx+HrMOKy?u1z+`c+-D=-Xb+*>EQkb9wvPJ9IIsn9q+P$%w+(uv=qb)qy#C;l3& z6SIN6Lv&)jIGwlxumIes6RXDP#E$)SVj5}`=%f<^N9x4Xp*pei2%Y#4`$401VqdKN zC!j76HWvBgpfLvikfVoY>;Rp(2K;KcFAM}u)QLxcx|4O{6PyEP=)@SnHeDwU07g&I ziRMW<(GME;Q0pi7i)ZS@`p79}LN8NwqAXb_E=|&joq&eGK%8G@{x(Y|?t{i7 zcsYhX?xvt`+#gTSiK}tndXY{XJWnUqp05)(E<|rQ=K;;IZ-`jmIXcl7KBl0L!H8un zz!~{HQ!z_`1AUk3#5XH+qIH!{47TXR=PS`OVp`lqqShqvKP=OUC!p(rMw!JrvHKF8 zSOR$y*673sYjtA!dL8~d2OiUOqJY>5=mjCSDEOnGxoo*ktO1M$j(~S~HR`OxOg196 zMJI0GiaB8IgL4S4{aPUk(bP~OE(0z#P>4Rjmih|O6|4|D1}VhOfeKOWuMkhE z6yl+33NgNdLVR0FA$BXG5K}~jxJ|1Nqp&suP6!I|8qN=aPOTK;*X9Z__&0_4B2*#f zLeC#M6KW{L8x<8|b|;0ntDQo$cTk8IIx57bIJ>d$f&GK_3egXD2iq#dC8Q8XKVLgzgHlR)j*V3C$U86ykMw z(lk+s4F)O1y#vt`)=yY#4u;=oh3Je`h*s=pBbI>k?EdH#TBo}y#M|&xyftdaqwmoQ zvF|X2ICg|W+%!%hrjAvJkz=5TyB)aOKT;t!#J#^!A;v<-7oNVthcF#=rz*t7lN91$ z6J|V1A5EVcM?iv7vulUM1{BkI;AJ#EurylDb_`JyHtf(cA-L?0>m#-h((txL@m&B z1@5p$Aa6YG_CYHX8Xe$u-8$S^6ynlV3b8$4#J!;d?KKFt?9G_=bIZy+bfWI)u3H4q>LwAw;)x2=k%kf=;o94k5FSLzq^{ zA=E745UP%Z#t_^Ma|p5#4#7U$A@qoI2qh6aHqaq#j&ca6pw}kCA+&?9g{>UI(GZ7F z0~!x!IE30$9m2-x4xtP7zp$2{>=2$~eTH?#7>6(t`n`+};dnoX5CA^~!cn)SL#VUZ zA$(Zm5O!f@K*uExVa|Mqpu&1J)gjz5BX^ENXgv-6k9P>mhB^d`!66Lp<`Cq+I|OkH zdR*rahHP*M{#J*uZ<|AKA~t!WLkL;#5SFZV2=1j0VIVXwCOCxZ6CJ{6^tJG?LpYd$ z+3j-({q{SAjN{PDbO^dbm^^iG111FXRQ(5(bv6ygH3CxJf){7>L#fL{sx zRPa}!)+y8~d^bVZcRxXR`yfG>fwdtp7dqW>KN%jbW`hU*!IKGs)}A1AIFKNWL=7i! z6}gd*5`-O(6NE7j!GDqq@X#VRK`?;-2)=HCQy@g@Aj!zc962wFveKzNIW z-@>Sq{y9PD{~d4pBSF~nGeH;(bj0~I;%jlg0=a$Rv(@Va!3Ss$-W$}Ljd~ZqCI~L@ zGQARou0Dyv7GMwdO`&lP9_E0r0w;coMp!aaBberBg!@SvVIQyo`xOZqA#$2VxI954 z6da`yx(w0?tNUn#vk@BMLAXZ1H~hlqVH)8C?v^5_oe5`P#w3j(pQsUj-`=HNq6c=0|CSC%C^bR3ikB&}_zNiVEb6DF)YlI2d@9d=!CZpaDokqA6 zrV&2EtF;+84H0jp5l+K{*iIu%h0ZhdQw(|Mkzb&@M%V=1)`)KgZ!z+IZvk#2jc`Dt z5o*=f2&b?w)L0`tMGq@6hZfK&2uwk~FF>s|f)hSJgZB=3i-R${x*Fkib&W6>Yn;DE zC<&dh@bS61MrZ_`ssj24N@z909mGnb$2{cd)f&N56SFR*5n5Kz2;Hk_gfg`?LStw| z2H>3%GZu9Mfg+*k3H3UGcea*Bs8kKT6wnApKaH@rm_}GuMkCaz1b;O&LI%9- zfX@6tjZhrDap+w}tT=ep!JCVExgP_CmR|#fpI(1yOf0SuWR2tzL zRyF!lz+b=e=tF^bI`7h^Uv_C5+;VA??zprQfWX@>?c(b$ZM{n_?XpuYZSf43cGp&y zHg~y8t4nfe+fQ<7&ku8HYp!!?D{sbmw@Z6ypG)i5@6tx3yR>Ix;=Z8X!ReUl80c`ofltSzDcZH7y`5&3OKyR<%o zUD}1cT-slqT-xc9OFN>eOM5iTrQOoOr9Btv(iZIR(pDMZ(thdf(h4yyZ5^B^cLk8& zx{XWQq`6D$YT(k2tb@ENF70sOa&4D(Rj^BYG1R49(9ESh65`TEw{&TH{pQj(#@?!N zX%)z6Rn4WXt#D~O6me;dUYOGdr?%e@r`G4YQ+rJ2(q6CV(&kh^-EuB%t>+cEtADBR=w$#O^SQT&Y(0p;90^Z)K|EN;r zA8Uz!tp8cOKp8J@FRxP7m0rGoW>)tqU&yQcRO|~D$-gO}{wscLYRQuM3B?^gf99>k zUH-4KRsJLvDCG6)KlvpO%g-y|RmMvWc=hWT6F(q&U??9xd{lJou&DT$(OxABdHu7z zkGHpXfqVvj{l}WW&##wXS{~r#<%gT%kb|}VkfYe4vC;pdP(BO4?w>gI=XAV%3;q@G z@{T!m`mZkX1D)4ZXxoXmZC9vt{;7aBJNMV!wTQo3lneRA3wRZU`+WbcW8>nwN5zea z`Il46xBrjP{AoE~)647FRC(2W`vtsYUWM{)j~NylKiW%ytNdd5=p2>bS%E)0`!i*9 zeAKY$s8P}VhD8mH`7;$$`ZIO#81xz&KS5q6e{2Q3iv6WIXjE);zoAhhM#K&q^k;tl z%jHXt7&Y7|Z-Ey@uKcgu0sr0f^IcT_M{3;gVS{4h$3(|ekH*Wy#}19D9v2n=XG9+7 z1@{HKN_dsbp96(;kUB<$b?Vr!eON?SuZj@J|A3SS{`C1To-uzeqYLBz;MXkRzdtAZ zzs`v`tL7^YwC>WjGu}JDkv~6w*x}cIeAL$eb@V!2BfnSt%5OLS*MF>ko$z7#_kKa; zpUl5ru{vB1x+41ry?W^GhWXop+THUUs#`R7)OB)xyH20l+@Y|U4`|TM$F%MCb6Roj zBc+br+T{{ z)37fuX@BES6uh^f6kDW>q;b}e7DuS1Yi|OjLxY2**3*Kd_R}@e)sFbN!uUD7UGXnE z2akA9fk(cQtG&1MGs#!VDpXurB&#Ncwoywvrf8%NU4o^0^Mj;MWrC!-uLGpT8Bghp z@h#0*jGu2U_m!&RXLj#;Swi~WNFn`dT3H%+thO|~PmttD4w43!3zGKE(@5VJYoyby zG}0>hdrGP6B^CWqSUNbmr1S_sw|-}OHL3ZNno?ZxI#TaO0aEdb!BXAP8fozCVClrQ zAgN-ZVCiU;`ci`(KGL9Zg{9S>6q0*pC28fd8d9|*^`x$|1Ef3i(chUs$=N(WYI;0C z>gyLIZ5ynSA_MA6+KvsR@#!U{Ig86m*TSnwrlTtS9ex4Q%%d7<#0w4nZdQ%dteRR{ zRS$D|9w?QE)+ei4YJ=WW!ULs70X3y2HEK)UR{Bc=PXtLv*4LLBmkXB26eNYu36z$q z0;Gky04X3zBYkB7QoCVlX;j|;$vM|w>e?|#IyX^+zgId?npHGN+T#wC5Vfy-~RXS+yDOGgMa_;!N0#}`1^Z?zrSbr`}?cEzrXq)zZbP` z3jDwJy{OMNjpVl{P#RDr@c+v9qBr_!{`_7v!lC&ueJ|Smx%!{qi%x13_~-YcCnAHS z%9Dft<@cf`dI$dby=bZQfPa23db?DBG;WLf&+kR=tPJ><--}**8T2o|7gg^F_~-Yc z*QWXZPv48KFC6&K??sbJ2mkrK=o=RFzx{jBUoV6Ie|_IR^jCnC{#(F*>2viWGe9cx z2=7&1Bb6!|_-}vTev}7Fl{W@TvGW6@R!`MZyN&99>-+Y#{u=505RFvdOCzO521=g| zfzsI`f&a_z+pEacQcgsm^s{ZCRP{ih6t`I;z0cN2x4#7ar{A}~7^Id4)en%)T=AFu z{nXOO$!bYnHb8n<2lX@>sZ!6N|LJq}G)67;8XO>%#`oKkmZ>GmttXwjR!{2j&R^Pw z&qU}Tf5`*g_21Nzwr`Dpe&4=nghuLqUn6zigx;EJq(Yqnq?OnFr582T(!wOQlD6^e;ObSPzFe4?y98%h5%`*HBj=yckXLWXrx2!E+twpu!aKRfbO1WI!X2K-x}t5b3Q(#WBh$2I)taGzR=#hb-s`u{IKS33N? z(-XS~O08Dn^BWQfF=bz&|TJA5UZ&gbtj|E6aPWek4Kh~8-0cq9!CHaneQfN{=No)3(N|#nkH#4hC zJx1W~%(m8(tg{29b;UK(SbLzTN{$*cKz?x2Mu@s_xhlcwFMoxYDZsk?datuJ1PsX zC6#zvItoimF36Vt@b$S~9Xslk>PIod`q9t%{r|;R z>WymuS|2>N%!)J{tSM|ipNCP_yqCE(Iiy(AK5k2!Id2_4U`LT}`u&5egR+{m%)*eK zcAHUai8cLg;CT9DOXoYS>F^*c`rFTnBG+2e!eT43jOPExTGKp^7Zn2&@(MPm!aOT# zi{N}?-;bu&^L4Pkict1s3YCmj4 z!4qs~q`M9Ix7(8GoPGq;eq`WkMI^}0djrVM5$GCp~5m)OhWGR}Dhjq3gxok40{3t7ODd2pqY5gB@m33a7?A%Q# zE!~Pb^=)Xsx*et2bDUV&{6}0VMH$dTKE4N%ZRiecsm#oV25hsYy(<46S1#2`|5|sh z*lAAsA-430pHKe-xAzVkN|WI>_{`>CxN+R{@OJ9@&`nuq7ul6MQ|s-0G}@0%5^;J7{&Xhn0o1*Bmj zpjI&frE$KqA8SMEp4N14zcrPlSyRw(TS|JsF}c!?k`rvnuAQI%wiPX0VoizGHYESr znuM-a|4+DT+A2fYA}SR0LYpkIOsMCMHK}rq^q=Y(l3vQt&y^aKENw`WcAC-CQ~_PC zu%b#OtN)0rR`LMuA*-in-gy~W4{W)!qYK#bQtln2^U z))`w^L(qw3_OvOoB zl$c;lHW#c&|0n0h|D{f5AMb-+CM!dKpR1Efi$2vaFePz*|BIFiNHN^}KjLchL^YZq zZ%n>_Ea{T|M73~rikWfcJ(q{#9o%^)~Cf4-A(1tcF<@)=b5~=E{(@iCP+WE$W zf=}?de`G~MZ)-a7Q$VI&7UWQFK?-&PO8#O=zpq--YQBw5y@0;d8q#f!#rwxC>BuCm zuem0Wd}Kp;3v7s;u%cIumb7n}fJy@eG;geco}RO$gC8s?{xU)XpJPE9-%V-9;7YJ-mH(suYHR=h#+B|~ zJ-U)%L5+MYo^dRha9&t!Vo6(GSkUa&N*q#dz!miTQ-9?-L5{|BDA1c(s@ z1Lp~EON!^5kv!Ong1EnPo67B}T9HPqP@^1cJ?e=zrs4N3Y0`HA)fQM$sT{||2n$Nr zwVhfDHu}t6BCNPZ9$t?TGLh~8(Ngg_0(-^G7S^ZLR(9s z9hUU7#gZhBT9RnA1sOD&Q$mzEop86J!|?)2oGG9rLjmRQ7SQB;D~jUr$y`m2D}Ibk zF#N1~$Yzn{|CO)G{A$q<+Kl%*dT@Tf6dgFFNUKI_(1UCGr1sK; z%%>XsUvcH#^Bayv-Dr3tMbArBsqc<1Nmd!qtX+mQ^q4xm-l{;77b|f>xBTDYYO$vT zJqlK#6S9W%(%qC^bQ{o*PX;9AtV;@~wQ0S3Im-4_{0mpP`#UkhypQ*Gk)WF0ij=-y zm#!Qzr%NL(sO6d|y;*2Rua!8T^qP=&Rt4n${=!a~?tgIQc0rcRrYlg&F%_Dp9`pOpMYi=H}T$9y@*nfrajk`Y3>S5>OW799&9n9humjR zT4X^M9|g2=71ujK0@`)eirj;^-%sH9iWa317ir4)rcC2?bZCO95gpg#n)AK|b?&p| zTxvmgw^>lLs|CesThb?a0ZrxDaZ0nIwEl`T?VKuYo}o+cQjEy)D!;Fvmh?r&imb_! zUTx*~I>nqyZ(7hzTMNn&wV)Lq+!pgJD7Q_YhMzR1&o|9!(PAqy5LnY=?swJRaeUP9 zdcPGGWOk3A*V>XUzqg=NF$)@MV@@u=%_xp@*ciTz@gYlcR$4$A{Jb}2ThbUU0gZcNNhdDw813KGi^eXgz>T9dyys67VBU?qtCD1MM4s+G zQKcgnHAwQa8l@dlp!rwJ{*QXm5RFPSMgPL1pIy8cs3hr&s?wHbO-fSKp!Q&8nmtL9 ze0{}e>*I2qY%KjBxOyB}|4+T>H+c!#`cauSkI*OkC_TywQ6tSL6;hE`q63MF{1|2U zJFy%K8mj-P7j3or1NzZ}Np<2hKSF`7aC{s|H6{O1M%3!9NA=?ksqZM)T=Isr)T;`Ezqz(YIaDgKEv6>QX|LFqS7 zAd=Dzr`1w4B2&0%d(*FZjcHZUK zc&HvvIEO^5Ns@J*619F;B)3RudQ75pqq7tG8C?jFDZ|6}CFqVR{Z}3GlxKDLZrqN7 z)#AiXDN%n19U9fCLZjBn6Q;`&yCF?06=kTXx&)T;eE)YSe`iFl+f3;5R}*R}g4Z{fn~^NfnfNX;rB5CvG*!cle!n)OsfA{=@RJ#thnZ21 zjyZjGGN)>B3tBeDl7g89{R}muFZ;|%f@A%1qB-T>GovGMdHXh=0B)v5+gA$F>(qPq0P0;_^jLrgQ+brb1%mJS;haOzFlE%8IFvogQ?OVTvd{w zNdpz>eU>=kziQBfTj8eE1o5&aC?6@o=dHzPTUqiCuI^8+z{feY2rq4g@+&b?+o3?| z;i`1cPlm2%OOf&(N%DRvPJiOW>BFgF1jdzM!`ezD9%#gHi8jos=|aL0F|sq0r%5_Q{&` z>#!aTTcb-ab@fP3PM@x3=~JtT0l5crzSI8&ZLKaGFP0$t00nAWs>buQniP?zL#wXp zld!{p+D;hIGchCb@-d=m*Nkapu`wBMGNG}_y|`d6NiuZ`^j%z&a(3&{q^Wuo;;ctU z>a`YW73myY>lb}`f9OIzX+@A3;%_yZ8r-rwXOsaKWpIO+z!_a zF^cGtqP5a(5O~#NaeXab&8o(0n;I-GFT!rcB8+PWg2PC^9hB;KvgyRMzmwyLlG(tl%%`&rD@GJ1zLDl zot7_Er#W}jX~Ph0I`C14c8TheyreFDnxI4Ouj}yS+#l%Ih*M*@Jna)#C27v9Yx=2@ z%?M2r>(C;RB|3EdwLV>cU_hF=1~i)Eu~XN8u8r@(y73ZZ@kNe)1gO)OtvaN$M2S2k zRq5LZZOWI@r=rvPv|Zbf@^2YZkcbiOi7=ovW4`}084{l?PbRWlbKcP)Wxmg}muht9 zDEIgAdStr7fL4|n(ED;j(wSpKn|~ORUSW17v3X;mM`Uw|<^PB$clKiOE9oAuB0IP#P75p%Wx z7O#tt)=~zoPj#68z7=nhTM)=KO-XA346F0t+ph?Re9NHiT7W+r3!rb5kE8y%aBj~- zw_`EFr*Z78X+?2HFU~o3qr|2RtLGGBUS=`8oQm+be+fQLD8!K~1vs#)5RD^>pzctL zO=4Z z37R1MD-E5;bZO(ImN}nq+RFNomdMWN%yxJZZ-{Z!wbkEJyur zDO04LG>sfBOJ3)csdu$1k)8(4J+Dohi*#tUj}D!+@#ZMK$`mPK{f>~RA=^l!sn^kHkY6gjv_(CyI@v}TqR#R~XZftw6DoRlW-t5USYk8>(2P<^o( z!yI}bvQdmGPKc79lN3c($MJ(wPL9( z*NOXOY13N;${i#^=SxM&{gEs=O;sR@Ri^b0YP5Q`I*;4bsC$tb3Gb-VB(qMOP!Od? zT`?MXP=Usp$?$wx7d|zJ(%cjB6qlkvQx5Yy+H_^QV4_M|*H!39xf*p1RwH>6FdPxTAmgJyU zEf=MH`?9cXjGB{$0hhC(UYif0OF5Q3tVgG34f@Vzp?PQ|N}^-oeKY|Z4U?caITa(a zb8&{_@UmSV5(4uOmsJS6?lPR*@*80xoe(y(U{FUb5@(m-PEj#794kS7R53=btiZB? zxwyDG4=R;KIIUNMUiyW7x0>-sqX%oxh|_qd9>g5!!GhzW^i5NouQ!X6ACLXs_=}N4 zS{{U!Wti330F&G<1Qqq5Y~x>i@D`mo{#aDp^lxhRQ{Hqw@aS(f0Lu4D+=VM zsz9b6IuY;Nf;Igck#ejPmv)PgqoM>A*~(D+K`Dw@BS{lZh|ye**^12)Jhvc43!`L7 z_&|{?a~mNQ*^au|Vl+Kin(h_I(o~NBIbY<-++LPuK9{5pp8Iy~--CiY5lSf&CsRi$ z+I3W(JXL!zuU(8ndt}HhU!L0P6}ZhRP@z8Gcaa>W@K{=JrwmDRJZ<Fa4R zia8)fF6C($Uz&;wIlPICOcqYqWFvN7F7JU{h!eX@xR1<3=du{sc7@}|yBM6korsMm zQn0o$4L$?2ac5RGI;%5r>0kzqKh1>Z-&{OcSqjpv#1`%s+rlEDv;PaGdW2#{PXs(O zqwr=@JkGUcmH>iR8woXX;_ad;R5r-Ey!hBo@wq5H&>Vs~4 z8{7lQNj=C-%fYqyVhq^!3rgA@xHY#Oh1R@b(7O)!6t^QmwhLQ_iqVKwk|dQZNmE>; zsFlZY2dX8hw!92MR==Ug=gDfh2xXT4MQC*m!iH2ss;dKAzlsriD@CJu%o<@QLjwm$ zlj}1np1+i&-v^qJEYga+ueCV%xe>0$zcJ^01xAhkjkAAwtSBNuBfTU@K*DNDtSdV#f6t|Ktyz4d}y_4l!CYT$Bz?7Nv@+E~KpJLNe=y zgslk8t`(&@{F?vz6e9RU9qdkaL!R50cz`7Lf6|o1uWSAmj?Iof%#!$pGehftw+xx^Re!MhAM#+%cVHs-lm7+;nlJqQAlxBPPz-3}5H`~%7wJQxcm5z(q+0f*Ad~itt2B!W%VV)mcM!&0)rOnq2`0k4`d$ASN*U61Rrb=Y{M8kx@;QUARYsVn>7IYxx0 z{#V{fQG{-|i%^(C3C?!ZKz3^b{5LmYsGts^&kE6LlM8+QTJ$UG!l8wt^r@d14VfiI zc^;y4mTSHVyZdl>TRpOZYOv!Gw}tvVY~at9cPz`s6-^$q>}$oV3!T_Lq6_;cccb-2 zFNSUC;p=C;xZU23Q|k)g#L{s!HwmZa6=GA*FGy#%;Gk?L+Fvwduwx@AzY0n>s?jy< z7w-3O!6}P&BtGiF{`v73uEI4i&qXdi-izdwqV#%#7Om&*IlaJ?PRD{Appx&nj7 zRKPvH8XvDVL3K|T#{2xlnBpYt=MA|;<5DrDAPr%i87S4s#;2rIB+ZC`bBrIBp8tx- zZIQ5>6bG+?@fhF}hnJG+*wa59CE2NnnU#VCGgDD@G83M?xsZC3iqmZYps5~sr1J)n ztsk-B$rs3L24GBHAOZu@v6na7Qj^Sp)t(G=&CG^OcRqgjltVYB3=#W&ph*2ARt)gL z2rIs?m_Kf~e!&NiZ&;|6jt;pDB=II~y?m}iR+S*;h_s4DO;r+B%fw>9x}Qk&h3gM`eiFxa| zow{>vAXklP(`%t8+JqrL{V`)rJnoJz1!9}=NbwIc>pJnip$(%t>ku)z5b_m2aXSA8 z>Ug8GSCseD~ybwn4kn%Xcy;};yYOE4fg z7aw@sCg)rP2_Dn`;&GJw#6&3GOu*8!N!Yz487Ave@OF9{e!Hb%NMjsgt_MJ3m=CJM zKA~bxC}eg<)`4%^<>z}|3r54PbjCRLH<`d7EXx3@vSfL`qCX7 zNP3RTW8R~*$PbTP{E(~ThcTa0kSm#rwpXbz(N4pj=NT9imxB@~uHO^5pF8jwr*1xm zR+k%=ZhC<{%~uE+{sume-a-3O3KZ_A;<|k%_6#q8!~H_MJyVRBxGKb6FNL>eG`8Oe z!^8L}sF)^Tkn~T?YfM3>b0+R&rb1dT8!IA;aQ}WK`fo0TfMZRXrQkq8EQH<}*zt!q zqvP+4cr9CjjA>Q)A*h7mo*Inhyflo*v-jc)G3Fw-rwu=Fbz}(i?tMbg&nSehFTkv2 zhtG3gNIo2-(os+x0bjm-{lpM>M1I7A?Li1zpMnTa-XL#H z7EWp9!TCTjT)a8fc^otPS{W2Sgd;`8A7VP*IH?zcCsK)6nU{_b9%DU^NXJyMB=p%w zVg8*+n3N~tP)!aj_ZDE;%`%AGc#b)4AF=&xA{P89M%~p~{5sfx)645{;V6H%hJG5X z5Qzmjq3|h;Kx9EG4%QZ9QFIOduKtW>!&uPWVtD5@LEZ2VihSBJv8)lxN7o@gv=sN# zv!FdP8HERbVvl<|P7mfeAcJZgxf~B^t$2(POMtdVBAnJF!9DFKhJ8zd#nkVZF7QKf z(rYZ1^TtuB&)D@K1nJYm(A@J4$D0!2v*joEjY(K=F9G|vCF0$oOcin-p(>)kO-v^y_hvm;CY$^ST%Q9(Dxs#1ETXXPhX)b~*3VA*&8L>8A zs6FI`jlbUGr1@uzkq^Z5>L85NjleRiWOQ(!B9WW}!!bn=J(q&agzuQPEEFOI?~yh( z9B=*O@oGpig7|iym9tPeI17#?`S^}Z9NeCb?8ppk8yJnOv4Lnxe~HVZ+~95JgPhJ- zjQq>-K0XIxqw=vms0a_ci%_Irh-)J=@Xj|5rSV_z+S~(PvR*ir@DTGAo+D)UXBg%M zL%uK^BQj%fd?;-0 z2jXR=FYZkCh3(KUSamuY89hId+MS7Yz1MLd@d1vA`*Qw>fHKFU+rSd!YH~l!Z8_;y zBwojU!LptHcp4js=5;aH6Q7Pzzw;4);tpyrdSKUz5Hxb#am2A2;+yKR?@BciM)Q0& z=Z|+baR{r8z{8M8)ON-pn8$#_Ruv%cd<+z}#bBX*91M=cqf;{;nR|X8_$S7NCu16q3+&WA zP?T~GN8cZW#h}BuY=0K^DHl-p@FI>Wx#IDgAE+!&!1Fsl5wR@|hN>JV>yu&Vk%T1q zw>UEOB2-3Q#bJ+IFno9)O>^B4Y4jL}tX@GzJQ2Ct$(WFkhRZIQ81EL2YpLF-b$*S* z`q#1aJNYp@=DdgL^N(n<{(?g{L-|-HLF9ZQerdeLqpc5cNB<&jjJuBcfmh)4_dY%- zy}+sM9G6zl@u220&h|dWy#>!Uee{q9$&|D#2dtlD>p^_ooom4#xHM z2~crN!`HAx47eEx=h5#`o%{}J<{#lOJrGCVMxb*t=gjqy*!wIJo5x4v`>iPKZj6MR zZZsTBBXRCVAf8cud!i$CmFu zP`)S@%}QUe>-s6IY}$-BW-Bpp+fwY7-HKx;_u`A~0eHAO!znNZEBA98SNwqb#3Xz! zO2k{MTpzDN{fn*8+v9|Ha}MC5!x5Y-JcY@tdDGL6Kk&#Z5y7g7 zcqrX!)noq(59V$tCciUr-C z&^mD)Q?_iwxFQCN`}1*(4r8~&Z5~^Cpdsc14BCSD@k0^q`VARL(U{FXqA%PH_O>qg z-n$3&t`l(o?@Z|KT!Wpq&iMQ229Bt>;~RP4Y|>kNn;U>BdI1Ps_7pDXE+RV03Cm44 zB2sq-v`iKwtYI7OEj@y%R_9SM)fFiZuHumJIy_I@!V6Jb?C5~) z&t}85XBE6$kE5jhJX(HU#qCd5I45(AZ@CAbL+4gI`ZRAF^C!vgogc(AirZdwyc{4$131zz*wxgFa?ei9dO{%Vg!#{hTSa@&^{Oi zw>v!VF^+5Bk&$TX@xtm^x8ZzYA|AZ6hUe`;__l61+$+c8&p;vi7f;1L;Vii9ibCbZ zXtX6pA^gI3NE~{G&u0!{N8?fiSXkoC?0y)&ZxkeY2?NDeKtX*o#$VckfDT90xJIM0 zE(jk=KcL?kHx#`+f{U%wu{253AziryU;ku>Wv#_hfY^S&T# z=Hq70ZU15Xdb|{2a2To!s|_ZIGqc7sv9Xv`vjB%*?19y=<2ZijB6bE|LeJ1kcz5+G z3MXvE&0{n0-f9TcMYJLENEep*=Gdk=61JUlfa4p`^nMr0-tNJAZhzLY2ccKyg0(+| z81%~u<&j2^ZqdM}QCe6%(il58*x}5%36S|SANmIzk^b=@E^j-9J!z-0@!2`NzHtTH zUh84P9d($^QiIoY4V<{Gi=2EjOr1FxZVxPy7aP@qP6i+V%`aZ|#{M$GYdfp!MI%!jEOQ~X!8t5$A?f@{_=va)j7P|G!r{}$Kw3u{%{H~!~GYwn36jPZ7;`Q zkN70~JQ#|@uR>966OK;1FmwiehJQ5YNB5l=yWI>?m$fn0UmZOr>S$W03+W?fNXqVq z+lxow)Wt9ib`C}783=sSYgWOA-KM9ENV_q!pktg zzGfQMu3C=j8J}Py2*e-fK!hy&g3r(WVZGaz*NA%Ku>A`R9`FqQuiapib{q0m7a%d5 z`)aQp7_GG!K6d`dy%B&3X+a2G^A#^X`eWXTmr(A#1B0ViFzE6*tj^qppw{`gy?GLn z^2YF--bhSZHX2bozu?i{FL=Bu5a(Wfg;>!iL~A`ln8iMx^O%qR`lF$sYyz`T4Q%t$ zLf>$0zBXh6hk4c*`78kIX9ZyFkU+f1{)|+Q*O1(F4kMS$;P37;$3Ytntoa~`hC$+} z?vq67F=hPK)x*UoOBAMl#mG%x&~n+A^QRX?0xv?UY$>J{+oIWD2}{#t;jPfgl2bZZ z^+R!NJ*tT8xq6V362SV3KTbV2@Sx zxL%S&y153PdztX#S>a=z8$LB3LAv7x_*@%_jZS88%aDiTF9|$3D~h+-qIjMo1=|@) z7`tB^$}{z+X?#6^oqIfP%poGW;zBO@26f>J$E&_~F~lw^)7Y6-MT}WA2!HIMa6v z^5<_t^viYZW9Knj!3njhGm-A!4^jQzqo0`{Bp&!<_!Q1>W^d8%^bn1MuHkw8MU0zw z20N_wVr|e;_%=_(&)xkImTZW(-8%Sa?g!^7ewe5B35T6OAv59)nv8GY^MC`8lW@ek zikWZ`a=)}!2m50bv1EfdipEGn)l=f#udN&sJ!Hg^=A%3kT4G>pC_Yskrief zG(yp91w_tiV#8Y}I5@@aPuKgLU!O`6A-nwjz7O8qE7W7Uw($pzwn^J}POV z&rKGup8sVZY^&IAUA96TSslolSK17rZ;d0Xir`qe7$cG}9H8_JE_>;(fT#sQ6 zpJJKj=_GbQrC>T++8^N~)nRTeiew)jq#g4?Me|!sSm=qj z$L?ZT=Q(WhIgZG-z3@`mip9@nAbi_Me0gMq^zcz9y_-ltGJb_Y2_ zF2bkkAd+qzMvT;YB+Xff-Ej;WFO6UnDUZ7oI#_E>DH~e+i)q~JVn!zJCjZ;jm?Pqu>`6igsK4p&~Qx=+irETCF-S2J++qgPzUSkkw@UWy$HOv8iTFY z!G8H%`ZAk8yi{7JdFRa{NZvKJy#nUF?vwiuY>MK>_N z)_V3?qMEfx7O|S4$!z|%7XcnX;BJj2|+yNF(Y8o{Z%c+Pz@oVTsS<<5053g3$InVaCSZvi|fj>OCubI9q5>#=^!7Tlx_khENcD-%W|WuO5zLj>{j zv)G;rHyqb}496k2(eKtNEI+mmY45h+_Dmc(i;OPLmKB7cF?+SQ^e}%h@)W z3bxpaLDAZU3zq502K4UBtP8dULuqd)t<+H#ch0O9$ zB|F1)&BzE%984d{HJ1>}9A{vT;w;EJ&ICI=9wQF+L;E026z4ZEo2DeT&n1~9ZLebA zr%AvsMGdXfwUFy-hVMak$bZrw8#ngDlT~I=7@>-3N!2)C8TX6+<)FXIVQE53l`?=B$h+X?JGvkrB_X^0v-4!0hRKuT{v?0Yg4=X}Q_ zkdMg~u0?~AHR1BUkcs~J$R;j5gFSJVaNhbf4n=K*`yZfp-UK{!oQAklE6kT(G$c8ycHk^cR ztw9*XHQAk`HSFp4P7YDFSm*Ux+tY&uBO%V;JEwSkFAPhY?4!KjuVGti*Nk-7SHSh_m6@F)HoN8IyY_4T}Wic~K5gYrI@$m6_Y}0mt#c*S6u2x21nj8j1 zOT$pDmp$$K#XhUmuoGEDELg69sUC`F?YD2S1GB*XzBFQ4y;EV!Whn0lS<$=u59zBsX`m&aqkX`m>wKUjl4 zav9eeGc;i8Y74ncL;k*SWgK;s2D3`zn)4-lSaONEdhTaWd+)Ki7kF%`)em=f7$dJ- z3y;t7zd#LmNm=4rhCagINnzmNY8I>R$$s6u%ywVe&$^aBV9A{U?EDH7xbHB8{6IB) zzif)p*#c+=7(o7*Ddw5T;eKuvYqI&s#$=sitC#Iy<&M|b$8Yc0-rh`hHChX24{JkF z+yKs%=8$bRM6p00qm`84ms`t1W_)G~HlJtsv58%}dX|;Cd$3vQ3G6|?HYPJu0gg$k zI51fsp&?r6o~wwEb`ebfSix2;`^?^Wo?|vsHnNKmN10}o8}m|+WaZAaEU=`9Exyvt z{Ieu+bBYw+7j&_;(<_-rS^_)#+>>RFJ;5s5H?fQ2PAt6W4tppW!uBN=uzfvE>|>`0 z*tit-=|n1fAri-&oxid%&M(=#Rj%y*0!Ox|U>)1(vXgmzzQJx<`?C)H3^vuMmPIvn zv8E;Bs46i;*(?Ke9+XByzdmLoU&f|wPhhe^-?Tkx+t1h6!N zYpmR13LE-dn>j1%u^_)0Y(a+$vkq}*&LJXb3T$PI7qm08Nh0{Rw}ExDQYP9zmJR8D zmxWcWU{zO**c^c-+ka;)yS8H&vv0l095dds(@uG8>8KpmeYT1XQ7>R0hQ+Ycy?*Si z+I^OixQTszX35HL>9BR#Ls*^U8s-ymfo;9)!CayP*!aCZEH=lVMVfwL9=l$%zYia< zU0vtc=Kd?#MRN>HbzaF(5%bCzBHevFMx z+rdt(T*ww59l<=u>a&cahHU(l0nF}{1G{y+jYrnGVe_ykMKiyf&sk5xrb}>_1IEJ+>lxA#W( zp9!_xvV_lh%urRXj6wx*d_)~PV3NQ-<$Phf*S|7ybZ4IpkFh_s1DM!?7@Xi? zFNMYjQ-wSCDzXC~C9$qt6qiQ)=JS@#PI||&u~Ki?Ls@S&`}TDv8orFZ(NJbryuF0| zF254~aflUa>MO8w#|AP(w`P`|S;S`TN@A7D$t*_wJ5zAH}HymSA?U=M}S#yTy*XoMx6PNAZrDO(cxI(texRPjhr^2lD7eyXgGuwZ<@kJ>{-Ij*vw-KDQ{6My^ z*O+b9l4d*ZBnW>FOb|X=-z-#-HDI@&4q>Ka7}GW2+b67IZZ;O|@JCHn_Ev$3XiBoR zd)kH38}fzn8PUREm2BZP&G7{Ss{5({*n!-D)*uqOt)*sx9AY_n4XTOpIf zOthnzxKR{Sfj?Vv?IBzJ`2tgVww9F%ZP=kuQMM@3LpbN@e4*)88=+yRov=Yh7A@yR z@OelT+tfdawX1Qgwm)Txqp!0y*G{u1NlV$%bMmZ8RP*&bRxRm#c zU#UJ(ICAP=cKW$EM7I25l^=hyCQ&!mm+j0tmY-&)*Ue@-?y9p>!4blgrOSm6j4g%6 z>2|`H&rEo4{!wAaoNV?^s*0umOk*yuzA(GZSJ`m8*=$9{LbfHvisgH^2>bM&2))0I z6FLVE6n;pUBy8G#PB`k?3t?MM6s!Lf$Tq)x!}1)Tu!nianMUO#Zi_>h^>{Jnv?5&C zYjQ&P;gh|vi0?Of>p7uBmbXy)TcYsQ4iC21?=o}!a+1COvz1*EPGDg(4gNolt~;)$ z?+>^4l(dVKQVAiI^gib~WGgBSBO@VSTWC>IDh-9KP^btgqs%fA*_BbrNJZI1M#k^+ z`u%bL_`F{CeLnY|`##Tkp69&pJ)%lqOPqRi3-=~wVd}XV7~?q!S1%+&H}E9Rch$ku zy$g4pf<)_eCIWqjioSk&!tHhkqyw68G3+L4E+53ikL#e2z7*r^wn65}Wt<6q1J`p3 z;%ud=m>TCMhDI5QZ~8JKF!d4Ecb&)dgrj)wybntn6QIYF@j@#b6>^V|TOcj!m9#}d zcz5w%ldiZv#6hI3&=AkUTcK)IhU}yR_}MoT#q-l~Q+5xo9zKnLW$ozkKuxR&HWF?6 zW@4bBnNVJ5Bz!m4iQ^fUMO51f(RKQ;xIOEL2(HT#>|vx>w{eVk+u279v{MuXdQBMK zxfP+^yx`v33Fps_LUYSoi8u68NVQxP6)suAZf>HOb2m&Z|LG;VDfbcA`^k%c?G<=B zd>eevOoNR#p(~a!HO~VMlbb}VMEl>pc0-ih-!4?HP8Z!Dy9&3UL1N}p1#y4-CCR;R zg>&c(C`igzHr*VEiJOO@>j{I}z8m7*#K$6Oa)mHmx?7n3^$~lFO~noCUc&rF8`{J6 zB5-UNtewX|$;JWo&u!tkwI7;441sO=QDI+rMyRu+q6~@Rxv#Tm9R3D87IeCuzz)l; z*kc(FlgdEEguCNT!(jZea6pyYU@Xd=fW0P(;!uUZc(-u4P*v0x%}tLHcc>6ut+`mD znU0!)ao9XJ7!l>(=!o`&R+cCJy$wd69?>XVK2wyP8YXrvH4&R%yvCx8(@;sv!A6hm z*zL3xX5|S8mWn}q=XzLdi^Egr46H5R1OHRGSaB>sO!FNkG%a;S@YpuA+&PEZFWFdh zZ8yef?u6cl-8i&xFRs`g#iKVD@wMs}nr`2NhwlTdd=@C2O1QYP$5@nI>xA*MYgje+ zIDRG-;8xKweC>7)As26BLvt%s-u}RHhkx*o`GeZf4wR+Wi!Yg{gwf%HLT1ShQGYQ- zlu0cYOx{KKtm!U(+Q|v|JZT}P)`g$rZsFv)TwDs@2?yze@S1f~G&olZ_alcyVRXDW zb3a(zJ?$#~Fir7g%xCyLtB02612C#v*f>V392XTD4H z{gfnPYJ9}BPCfB0<{>&p-au*LB{=N90n6^C2yn=Un(t-|yB3SZry^1OUmOmrZ4!EG z7K-jO#*3`|)$m{D$A@rpSvtDZqam|sC0?Ic1G{yz zCEU_MXszijtnU9o{F6pW-_#*0_7QZ@0Cktg@F=TPQO8inN#479Us545$fN3!} z&@@7{#t#xFHBAKTQWDVpfs3tgadOxjEmV80=g0Juk{ed4#zvJD$FF4kt1@lJT#lFx| z+!&gN5i_rgv9iTtt=3Lqnz&J99sf^wpBOGmp6ZIKRVw1l@t-((Lss-Pk`WI>e&dnq zH~7kb!q=;x@$y}v_!NCo%v8!2-%_GQ_O%J(ca4$gvHcq=UUb1o>n~Q8%ZWi>Wklo3 zE?oH6frNxlXzS4d+mpHCMQXkE*7iHgE^d>fuOf5Am>Ibn88PFO2Q zi?-lz@LKu~5xw8wo#qEjytzwUH`^|T_{57F3;o6KWPR~w${(aVK7(P_7qpitiv6SI z#Lc)a=x=z354lZvE&UvEZ<zz zkQe7JNeSa8ACRisfChl@aQL|S?h}Ax2j^7$8Y5Q_y)V*e~`CNO4J^b z6MLS^im&VbpmI(-{JYga`{O0lq?|!i^LYebw-9!XI>N77S?p?%7KR_BM9+FD!G_3) z(Ys}Z&v6-{quhlw^;fX`bq6sw<s)7L&&_wcy9*+pw!E#rSIlSUMsfw_5v4wgfd3NjtiUE5lTU%~b`V z9VsU&Ps$2?Gg%>>D=juI`ial%70zehMPt7+m>+Zqmjkll6TV+OYS}NU+7m=e;u7)s zhNnm|vJk`NrG)ZODe?AI7iQlZZt`h(BP(qevxtms!IFWzS=iUdt%k@`qkv^`c4nh&EzU~HN= zv}v7)dNM|&*&B&*hq|zK*H6T+mlAo4WyCOdIWb|Rf>@xdBz&Wkg>|utaCxOFIwY|{ zj_br@=c!_(lC4-N$-!@BKk=~b8*2MWctoR&@XwMJ$2QB0`o9XIMn*{l>nn?&QYu2a zCQ!Ki87t=G3>IM(8Y23NjQBbH9~74S#(hT_p}bdG7$Hff6|5o+R)rHWARqa;cs9Q;+jwD^<$8y8EzBGCT>l5e)5 zxw9FaUz(s=+ytAbmpF7nUrd^#BkX!;idi{oqE@eGErn*5@K>_ZxOucH}>Ew;#$Aq zi`j1|&65^;*2{_)Es7%kgNg`qRu>0VG{wKQnqtcoGm&y#M=Wa65wYQtv127IUWEOH zPsKl!D#(b;FgfuiM>2K{RE3$ThUjOZC1fPm7PDVVyc#4g;<`)bjMZNd8u1s)CrgR! z0TLZoEiWFQkQa6L6~r$uWwA_8T@)YE6s`-jgnX)&L||!&q*LFpc48~it-H`E88c(_ z`WgC%eq)r8 zqG&c(73TBRMNp)M_$KL-3)|Ji$4)hIX_kgKaZ^)F@2Mrsl_YWBHO0Ecnxcb$h10V? z*f2m*tTfON*;}Z5m>um8P)WrYTA@G)2fvO_3$}9Vn+M)=qN~ z=C`JbOPfRyoB-Uy>3kwGY;g|OW&b{yB?34#MF5e37{sx~Hw7bVf| zrz*DYP!}WLYKZZR{y|mTN5l5>xUYW?I`R*2qTw+%hJJ*~mESPGAuV1_k{4%>DT{=6 z8X_#Zn=rZBU5rrD6(u@f@XX{6)&!ShRooqH41R!g@d6K*f5$j$8PR{Cf_OPiRU}Gs z@O`jE+key-=`w~QZ;GJ^T>1jJb+yPDauwlgZ(xT^4dnK{Ld~(?s6Q+(cFa)~gTHHu z`;I+?N~)2#-`7+G8kvcb22*jwwHb~3@8E~@4Y+Q(jjNiC=vwm$(YqvAH%VD^z0(xd zb9;#0ON|Bn>?yWr_ZH@o<4T6P2r+HPlE8;}#qMBxP7O*sn=pQDC&I4CiS9D$V(Dca z;Wg7pn97@rms))!bE1{dftC2Zs*l(**hR>2FOhiX4NP>3VOV_+XXWdlbmkn!cO+nv zZ8+BFhhWT%xp*a3VO8(-@SK-|8#%jCxp%ZEvj`L?oUO&U){pqzehJB4+o5|j3DY;G z!)n@Ew4RB=3E5CI?T^8et4S!2+Jc4+dy(ikQRHe(5I-zOitg?@LN2}<-Y)s*S(y&r zFA-Z+H(=L8+!m+YR=+#@DOp!&PHsmlM;iUp2P;HJ;?jE z3%&NGqj6LMy6JC3#Qlxv%F00Y`z%<-=Yook;&x9Rabc#4Sg0>2noBR^)~?e?j?G5g zs(tX1T>JB(i5NX55iy>r_<3>x8l60glM>cM!l1$ z-jR=a?{=X0W-`wHO-4uXR*b!wi=8ed*s<*@#+|-}kG~W|px#I1l(oSsqz2>VqqSSpW0`I4z=ht2MnR6VP5)IM!?_C`Ce2f+iHSuVrg2daY&-i4QJQkXIP@JH8xqm; zfjgQCrem9GEXHk(L69=w^0tdR%6{TYroQDD&;R3(qE#_MvlouO8U|Yl|H(V3EOI}c z#qVn?U|{TlPvv8vEi((Bm(RkSb_G~y%ENAV7eA)_m+!f$fWs}_5%$LxHCuin)BY{4 z6nw=vg?zN$3W0&yG<0higBw)?QFuiYV|S=SKU4;br~Kt{A7t_Fml9eOjG=v|5ijOE zM%}n?u=PFy2a6S;ncmpeHVTw#iEZQcaIjb%_G=U|6!N$=P6`e)<*;m~7L47>A>FqE z12bFU5_%N(?r(s){~R2(pA7dgL-72O0URyVk#D7ph%bt;{v(SIKjdH`uZ7r4H8ALY z5iOcGuxi|SJX^K{(_LdwP#pv>BR7;=_Jr>fEi@fc#pa%>=y6{Okq?v+w^sw>KDJ;` zNd@%Q6hY?lF~smJ=p`nj#C4Qna1U{9fD6Tib$Roy(?d%Du+dpxm zv<+1Y>R>&r3UUumLpw7E_tPX}AZ$6l*?3`=+faPT>xVdF8|dG%#ce}7G)MJEr;fbP z9rzP|)7$WO!UKFgUXIC41u)gjM6Ub>SVb*>^Nk5uAsAX`4MVR#!{HW1sOcDilO7`> zQ_>slJxuWBkP5V3OJnL?Be8If^yv8n%2UH*9sn^Q6Sn`M;SNP0t1x z-1T>SrA;J`j)_1q5)hT(g^?{o@Z^apZVv4RHs~{-G_jT&gkR!Qluq;Zkdxe1x`bag zz0Uu|HFC2aktiRU1juFKM9VrzeI135wzlYh!Vng_<*}!sjX&#lpDWs2;Y(%C@-vQS zdF$~qu6n12kNCU^E(vLH&r88fB;qiij8e;obw1=S>b@A2PC=hTNBglrQx0bo<9(c z{I6Oy9}<3p=N-GlXUKreV>N(TF|jj9%e=5joWuK|%vod*$#r zr;~puY2`)Hjhq!f1{HL@5cE>*S8x-|_L=PT_a@ZfqK#j3%2{4DbrYjY-qt^4c9GRznffVFQQz=D2^{ z1dDQX@q2AG8WOIwP^G$OaaAx-j;Yg=0q@_g6T@SH$G=u!=1_ z)?pWySIFgt!%p$(z7_mU(tTdN=MjIlawF*O3OwKMgQ)k82-skbp$Ftp;&zX3G`_$` zp54VQckkrgC+y)JwuiVvd?_!Pf1OA6t>zBxo3J1@5k_t+aU|In$Mi!*PC2W*;BXv4i*3%jRDO7jTQvv;1UdC2t8$Mz2L%Vd1_JW%m}Lt$r%j-5&~3 zq5;*&pIom0eSWj5h-WOx=2?6fSJK?ie`gl(&R?ax%p(yst9M|*)--IaiiTUrd<6M= z;&-z(95r>ZFhmNnp0(U1=OoX_$mJQuySdKB16(%y5N~wcf{K$nFlki=HigE+bj}*2 zfA@uN7RN$odn}ja^`}Z{cn)viXK$V1N9G^lHtJd2P$7rQckKh0cv4#f(hxo*2?tlK zg~!8SxNQ?$ zSFeL|$O5>`2*kKX30BP;hyTKy5HhG2dN-(G@WOX|!HOz=!0!ZC(m4h`W*-`lWa8DP zRA@ZifUnO&k$pB84z1IXIX?hD-h1G)`*8S;?E@D(EhMh{$yd`O9-%QqvJT*k8S<7$ z`%f9ZW)e);n$5SdblyHcou`c6!q1N0&lfip@*I`({C3k-ZWB_;^KMk|0pF9bdB6%x zvI@k2k1og=r;Q<7@A2t6xjZ&xC$Dwb#FrIsl@Q@q(RjoTedt zR~79+rOi%MnkK`zC!4O|L-^U~5s{IT2-J|Jr|w_lOWSKQmi^G4=yg|OrN zW9fN*w=4(E5{`B-B@>4ANjP#l3X^@j@b#7jWcsT@+PR)v?i{kMOF+d-&Gs zE!;mhi?zQ!S6h#wpUB5Rwg^Kg8s|E)RIj|TNRB% zO>>YuYYYsk24MO$0~~uSgHv~W(JP*?V5Np4=g zi$7J%=E2vCc>l>4c(i6E=d*9}CcP^DB`F5q%-7;`(n2iQH4dH^tdW_j2HAeiJYYdJ zHw`$(Z~r>L_eP!O$5StJ=~uUSpT!TkN9SX%^<)z){w85z^Lm(VS&Fdpo_O4M0CYcS ziUb^U`Z9f`?!uL+PCnb3ZJ-%)K1A7t3+F@O2_vT31~UL0?S@b z!dXiP{FtkUx2JybtL2aQ-}lvgAl32T17Gv@JKuT!Y8mwGI)Is4`%%3;3vY9GVy=EN zW(KSVd+LjIf1FVM)dZ#O^5}p5E8o=Z8-KL5lg~LOi#feDks5jkvx*PntjA$|m~jxh zm9nt!eKJ=4j6{NDt?EoEkWglgIbQk@ni??wpoV}Qs;GL?0|zD?#rQwRVcAs#*~Q1< zur6Ps)3Y$RFcl?JV_=a#4>9Q;SiN*OeunhN+)N7`HtmI0Hxr4+dJMDWPr$71G}OPH zMuXA`gn!P5@xCks$Zf__{Wt`BghIo3E>1T1AvaCr5b=b9c1x)tO#q-7~2=bBSskH;XhudI& zgc%ke)4+$0zx+`7dw%lWb1vsw&$Ir0<|ChX@t`~Z_=@gwFiMug(RDI-tvwnS_XCQ3 zT`=m?Ks@%c!q75fBwA=AJyIDVw|?`F&wufLii&9Xri`UG)Zq7115u6|SQa%IO^+vG zefwz0eI?xdIv5Gxte__0slCFwBPdM+-8L%VZ-NfySL$hz zOj#F*o}p7P{@p}4&Sw}szzPQ*8{=7;0jeMBz;3%Ktaeyno{k+JNA$-fr9s%G9}B6X zXe3ODz{w-aaD7G)+#dPi<82Pspq%>6zbqegK!P8y9J!5*+m+JhBl zJFw(fCc;CuLV0c~C^H`N|01z+{35JAKU2aJeNen|5+Xw;!X?KO(SJR0OWIE|XJcDZp)46I28@gqpP9ey$tamLtuV#F0{jf&{uOd?B&4z8r_>l(Zp~?8RJ`rzH;_#s~275Mz~VdT>bu}^zR#+^Af zuI!I{%bXy(4THuUSA@tiy!h+}>(RrI961D8D)xBTL&CkxZE&ieHI7WTLdSS3Tpn%> zuj5Ww8_1x2bp)(7jmC`m<8bHHSnOzV$Nfx!{6g3N=i0Wwq3|&pjOBX=V)L2)aINbP z+Z*HYUBMH^x4m#ma|)dH`JwTU4;Bpdz%G|@*z;;6PFFFgt`u1Qn_-2%8;lOP;NwI` z7`>j2(zH47E|?3G(ev;saW4E4=D@8v08;KgNROHX#ku2g({cjRr6xkNd>o>JMk8*M zfXd}CdN(B|vnzTv6N7GO}YBRLVHpBc$8V*GyBUv>8e~-jr#o!H) z&RCCRN3WK6sNLO&=3NO`KYbIrZl}W7BMo_-saUl>35^RjLhfTM zx&qguZ1#Gb>MsYo3=QO5(#5|Y#^@f^8~Gpl;QCN2Bu(vuysH+-GP6L&hhFI3+Z1-s z4KPnmA6*v>knOLG-J^}MHeGVw-F|SHJqVqdgAr;x1PkU2M#k7d2+kgWp;`Tqc2t7# zvF7;UXo9=yMwq6g1G%a`IHf-XAF~Kf6+r#G5%_mu1WbksSYG7d`omGB?u`2H4p84@ zi_t@SBVOASf3nT-;rt-X7)#JG9F0@*;~-z)ft<)m_@U^D;@a^T;5Zt2Hw8Z49gfv2 zhoHBT9Rj6$;gabPbOvzT-8UA$9(%!WgEy|b`C;O~shHa6hs7g&;ov<9Q|^w($>C%0 zF-C$pQBJ6y+z;(m6QKKI6zZN&z?Gl=I3Pa*M%95>GGR7EjO1vXjX~*w=!_45PR}X$ zneTxQZ`>iXkdWHugFZvYp$ErTZCtpA(&$v zjN}dr8}1+`tS<8 z70WSBZ5ggQF2s|g^I=V!{oBlq@nL{FXpz05$^H_yhG#dER#!aPj#3c-{Mi?Q|j zGKt1pg|lncpxh)JKAYFzNwuV(9#24Rr8hR;_m|{o0LEuegL1XlqVu~r6 zCXg~RMA~2-)IL#$$0->UT#ce86I_d)DVG)tXhk4cT9#K+|E9#EpZ+hW-xeZL04aOwyfYI9q zAzX0~+H?D(<+m-^6AKh48^SzG;^`be&Xc_wd91lCCYR`9$^bJ24jF*baE=G-7^bgf z7j!4I;(6f=`WveIRUys3otDkv_>rdWvMIHESh}N4X*gv4Rq;6mkp1KfD zC{M5iFde7qC1z2>WOT13n&b> zhvV*{_*~|W4v98>R51sa_s++<4~x+t(aM*fp5<>B*YjWO2lowCg_K_pNj;b$GG`m$ z)@uU{bv1+Md@IS;8ID-EY$QHZOu_SUL6YhWNzLbYW$x=KxXh0g+|nYQU-?taA1T&w zml?14=B#gg&r&J8jFUppH7Vr0=;Bh@ANVMbWW4RfYy!HG%o)DD5FJC{x z&mXPe1B;&WBYoa;uT4_eFizrwEmTDEV?}tW$U*nm557IQo!{79%}$NcRKLNbbkzBEUB=`0 zt>-EB+1!6-Egvvi4Yqf?LqSgB=e##Z^hGnQzHNfcE`6wO=#IH58c4AyXX{lJ_{7O> z+}URXfBtL_*HyU07pbUX;4Twn>sn#nuL00G*B`g0+hMJP74ET~IHh9<(?Pe{O!~vh zxHX?xwvi7CDBvi(#0Qy5W8`f;gsAmFg5+7Z{c*&iuS1|RZwRV(*h4lb(bQzq7ih}Y+(L=AX=iGfOXCod&(6{4m(P8NhNcU z{l@;9_v67Y=kSjd$5Wej@Z*~ri-zF>>#nnRHK!x^5X4DNO% zOmAc-^LMc1@8ZUD)1;;R*2NT_@MRaDz3ns)RIcGhDQ|hd3^`Q%(uSUi3GUn3;%oe1 z1RUDQX4PF`C)UXDf@*8tq%o0qzMIW&nl0t}Jep?~Ci47>9b7)4fX}O_;wC-5bCxU#x~d<0`)ofm+s|QJE33G1PVlkE z%lXIC*SW=#8$2PWlAC5cAeWTuRF!&v&Z zoOOunNzOI?)yOY|HS)>>&v3qOVy12@f zO|DB}lXt4|K^rIX>QPDDwlHMI23Y=xnJZ~kip8dYFt}Gkw`Q4jm4vpilMrHGxj+gl8124En!*4o%{Wtjy_)flI zU#a!$XL50RK|WrOs7q6o4czX_wA1&o?(?eIOk+8|q~Bn^rhG0xD7Nvzhh*8jnR2W( zQi`2B`-^5D{Y6%he<-W%C!Ov7k^E=0(Yh=x_JljI>yd%XOe2HU9G23}@NNkU`4IDs{8r8u zerNYy{`!40nFqh7hH)=wY+x%jRasOcgaP)MjV@jAuJ;7qPUV^88b_BmaGT zHlGw2&!cW_fbY9iCo}bYCOIELxnA3Glw#-O_ZAg(+juxvi-LLX2Dng3MT$X0RyN#H) z>}DqOlspvPC|I`hD$K<;YwkAeyn*_<*>wyTFeGfC59mmlh~F%z{}+DSDgTcgbS zPSRlOTPCx04JVjU*sqDBUIc3~tC!ZyV*hlOYkrK?CAP2^hu_jM#jj+PAjR~nm08KMZft#x zKFiTCW(}!E>}ZSuyE|N$b>(ZbEyX?9_L==y?-C!jKXDsd=lh+uY5t@4mNM+Kf zs?OY&=(2`zBPLaA#wN}&W8d!@u?G+d_C>g+sL4;xc`G zS3|#}o9WR%1=ja-Zx$^C`_EwklYf!Is_Txjg9Z=TYc+YE^r2+%t&U!Zoui)9YpL~BJIOzjVf$>Am^`Vmohseg@>WAO*T{|?`o!77i?djO*Lrqt z^>((k+es!n=`KriKTf78S7~I&6N*{hNu$c;nC)L>X0lJ4^$FK!uMB&!>02Dw_hlYT zO=%@l)ktH8{~cnhM_gq|YKQ68(Nh$0>K^^Q_lol0|Dcys(pr~ywfE%9tn7J>e-Oo0#Z#qg4yiX^pp4)a3hb@P zs>57ar|dG;*>@*%Xgkj;JsTKKd}m9pEAfNfb-DN4Uc7zeGRpK%rgMu9(6+wkC_kc- z#&2mSA5~5EUelF5J-dnSpRJEhbeA$I>F zDTiOD{AtxxcjONpe5KDKoJTRYI~$mT#~~)!@Pt)#wX&Q)znE4pc|OoHfgX5e(b-Wa zXuV$r?Y(uEe1jj7)`3>48l}v(PO@g5p)=X2mlk(UA_k~G^N%Men$rS%# z7qu=gq}g%&qZjcyxFzVnNI)#|aMmW;V>i)61??Ps}V*V(Fu7AE^rnqOIv zO5^%(r{M3ol$mgbHu~Hk?feJyW>_u4nU> z{9xrVY4pN=EA73TO|2S*q!@mYJc@2nujvgmI_VX$`M)W$MT@od8NzZ5masn)cCyOF zS6H6ZJ2rFmR=T3OgFbZZC3qd8ac(Ec|H>sAn0`khrW+|s=QYiW{X^0x^w_)*VvM3$ z`{+ED(5;$vTe^>G`|Kt2)j1RwoktGW3+aUY844P5m7c%6OGS$sXp(*__1oP=*RSfc z>g%JK|M q^gKLJTZ;DQ>IbCu4R-uGmX@PN-1f=C(>GA%H|q+F!`?uZ1R9YHY?*1 zE2-;ZgH_cy4b|i0FZSXq76wrCl36sR&uY4MGMVh%4^pS{1^O|wo%UtxGo!CwY|GEB zthf4A=0zWwS%(sTRN9@dxn{!O%$!A4Y%$TZ4P?4{3wfX2LmAT~dD8DZo$vFSo@_N@ zLo63Coz?^FwZUWdW|$Q3NY>=Ndl~S;UGr$k(<575BBc{Fd{Y5H=h zoHBKOQ1j{jOz&GXJGJICGo9APlndnfcMlzIb8QiASQ|?|{W3^#Yz|%YE}(wOh4gs) zDf)Eq63w|>Pe0eGGdtsPjM;8xsdg2tuHqZ>O;qNV2FuAgCzg5++e}A2vMFipQR1&p zQeo{G@++*M=FkQjI#Y_BakXUqTSM6H7g;Rj$z2v|D8(mF4JErnG1R#^nY;(=rtRgq z^fIxKO3t67rZtu1da$0(J@`pZOM9?ir(BqHYYbajp2wb9)v>O~Ra9|hEzNXKpc}R6 z^kd~7>UJfc?w>zFKVFv6rSa7?DB>M?+pDoLidM{Z+E{ikE`gn%bDXWPm%PAU7)=*1 z$J0ISR4R0OgjRjtpaGft# znQqQ!mTs%UUk{%_Gf&T;&9{Q+$K%yBzke+CJC{hcYtyLtW-8VG-Ay+#FOckc4d!}o zA&We8l$GW^W4-<5`LmHT=;Qhs6nS|*>0XPZu!uOCe=~_{TQ}1(_Y7*V*hc1t2Px{< zQ>y=C%c7=iV!zlmw%e?OO?p0q$}i2JYdH%j;zktpNZ&}zGnr0~&!BTsJLp&bZrU?z zKb5+lrQ}jY_UQ5`7IJzIJ5XN7FlOdT2#J` z4u052{VyJ+v@z$&tNRUFP~A-JWXFp<*IU zJeW>v2kas@jePp_@hrt@UZ=)AH8g$r7n&C_khPgFr^}8Z)X0#~4~R z%8zC~UqM@P5^3q+EE@8Bih;)K!bHQ9PuU$)zrWVrESUvWPFJ{&|^Vnd6 zMU?x_mnIJMprN-sXyRob`Y|tnEKTN+Mag{HqOy=a4w+AGuR^G#G@TY|-lA?BT$plI zGLtV0p+)*WG-Hbgna%Pf%iBKWpEZqMo6RNpwh;2oT0|rEFC|{MlpdEvQ02Bmbh+|1 zv8u^z!=D8d*5E^fW=*E3>yyYJZwmFZnMTtU=2DMyA*7JIl4o)tsQcO!mcTw|pXQqT4MQcZL8}32}3niZ@dh0=3 z&3)HlI`II-#gUjCCdHGAGjNH=N`n zKuJx4Ofv+Tv<@fRrs0$%8Q+GLWmNRif;HPE(ZOHK$+yIxI{Uekp}Pyclw9+aId0VB zH=}}JK}|~qy?8L3RDZe9qf|j=10+~-`4gR5zmWz7ET$Pb-lRL8)4W(`V*Q!PIUj43-$76 z60Ret$jqI(oZX2n7vy_&I0aic(dhfbNg*PD#;r)8gUc6_<`r+M*XHy|a~M(Ka2gxJ z=8K&;kX6H`El6ap5g7!<|S5*$D&SXJ$NeP6O#dOTyhqC?%vUok5ZXOv<#dd<0 z>$}ss&!g#C*Jw)oH=267xs%B-cltM8a?Ek3S_xjQ{Vc)LS1EL2TnwGt>POdA2a;E^ z30Z}kQs@o~az1TKKhN9J+0jmVKiNR62*r1 zr-0r)>1tt5vbO6(JJ#Bg>*jv6W~vRfmY9>iY#&-P!kaF>N~WksX>|Ez0u`=bN-y2V zQPYclbiS=8sTlPl?`kWmDd|t|-TTw4rPido$eea&+K{Dj0F91Jp>6hQP)Ye6l_mX!U$mSnx`X`g{T#SQI4BkuL2Av=4ML&rE;)_*gptlmV) zbE0Wd?R08iTH9dRiO=Z)ah;6qf9@(1~-LfY0ZMO7K*i*NG_T*n~P5zyhbnjhHis)%h zFLcwXc|rnhn-WTS%REWy#SjUWTT_TdA9`7BP08uDRO(_+%iZj0!Dd@3s<5W#z5CFl zg?&i5K8+$=H13%ag%@N{R?SATQQ1gKg4UCk)LgPX;z~Qu8d8{@2K`>GK~2yie&`=&*^v+H`f5IvKxAq$__DXxQXGC~$+}5L8!wt#pxG{xF;v3?OX-bMQwT9Qx`dM43_VZeL@?s5@`!A-t_g-Y*W={`# zsgkM82e-}=Dbk&wNa~*z=xZMZnm$^V4peu!g?V15EQMrx@gt08zFS3&Zx@on6;I0C zVn>TBlu5<=gPY?}SyFRVBn3M~TE9ktB=5}9!gpP6qc@(Rd71I_-f=ZWTCAcw>lV<0 zD-&o?USIlVsYI)5zqsA~Cqvhx6e(<~qC|rz(3)McG$%}o`rSNACaQ5XY+l)Ig!lct*E500vY6gck4S+UcxICDe;yvEp}2SeIo_ZmbBL%c90Y-W5}&wIW3M| zMpNB_Db>h>F1T4zc!D(L-u~`p9rQnz&OENh?tA}EhGa|`A`!`usi<_=YwZjbQBE?H zv6C?}CetHRR45!%r9(t1Davp}BvK($A|ezrPZ`SmyZk=iKd#rl?|ZNJzOJ=ya^&Hu zjn4u4GQDO4Nwe!qw>@?7d-2+`9DhLmao#E^C(M;o>&%fCYT>g{EjMX=*ix2cDWqZK zPu@Pbw*1|oo~&|iAR9U~l3MfnGR&i{Y}Rb2oSU^wekhnDSAFo8+-j=4aBY}ukFO0* z-D}CqFaGkShBc+)Hh#_yY9O=i8cFvz4dvH6_2jKJTjlbvi)E8-{_;-zEP1ZYWLZ$h zO*V4xC@nkHl4n2tC!h7LEqDJhkgG%k=>*pAl%Z_<^o3NE@0D`jVmZaz zM+R>nBSU8olOxjm%B-&VdAp*54fj7{*QB=`nf8%;ul>TI`#$qDqe>2_D3v9{qT~ea zJeiH(XB~AJBYWzG$|jQ>q<0GwIp};jTTgq+hRffwh0hoM>iLC(mVD;2x|Lkk;4XfT zuuXaz&6Y9w6Qt5^lvLxt#3`F=C;#?nCXXaMWp%Bm>^bBkH|zX`uRz=UKXdM}N{(A~ zLw;McNw%0UU0y#sR`v_>l(U)+ksoTis-dV8vU-+Wu zXU=pwtjP;&awyD%B!-GJn`TiH>L`9JN<^! z-M{iw(;wVb`G;q3`pvJ_XUL8Cno*A52OrBHDLY2GNv|o+(n-}zzMp6)5B>bacO5GD zdGZ^MZ}W+LO@44=u(zxgvisJP(&NTz+1p{F965WWoHx}?7EW`Pbu(;bqE$25=*l;q z(5r$+->KjQfgd^b-B-S3^M`x(P)K+8<8qh98hJW$qICV_AqNMz$w5BO(xb#)?)uqM z);sf;4~>4!4+|^UWY}BQbNtHH#y|MsT7}%O_Pku7*(obq`^)}UT;xhKrF>jwD^teW z$lj`Uvg7DKyfn3dEjry`%beS6Y;%w2HonJYihFEfktI8XZkH#YO_w3%PV)6BrTl%< zR_gukAtNo?N;&@nH{N!Qjq(cF1I+Xv#q4mSnCqM`W*>+1GHmNsd8+ATd3B_dbg$W0 zavfVaIJAch+1px9Y4nz({I2n}Ik(yGS}~hnF6N?h#cX=C7<0cU8`*A=eaB6bx7Rw! zVU-Tj$g#JayP$_u)@&`6mTx)d&MgkNz0JW%#rz4GyGjhBmm z4UneB_;svfFFE=_S80H+{f+B<;?Ku#v21pm>&F+f!N_|&rRziPX!($PcFUI)J0hgH z+gOSJoIrN9b&y_Nd&zBSUFC+(t>lERUwPO5TindEke#*{^IqqBtatk%yB0j;YxwiP z&X4gulnQS-41X2kQ@1`c+0a^MMdNddp^2RQ;5(Ow+~EsgPaM6?*@NzJ{I!RCFYh7S zye*Jx8%M|o4ZY;Sx&3AOI(ymjMo(FzXIB|Cqm?w6{)hc1-{)BOLf%|e$jM+8Hy(1; z^fHbba#J3<8ZNhkweQnk9tyOT1Mc^fZSYy@Z+lbOdX++E4lL#8|J~v7KjFQpm?vF% z$e*T`@rmPwQklC+4*%gTe_e8v4OZGpy3$j=ecn~RyJjZs&FaWkXP@&CumRoga;m17 zhgdx1?73yU@l}L0iSw5MV_f9RtDPjRX(iXH@i}E>EBR_&3u%a7&knAGSH(GgW0}bd zZ)LLKu}tn0pUJ7u*UQ1{{p5)ZCwXvlCpk&aRBjvIQVzJ=Qsya}$;@|pvfZkiJnmo? zXP?Mqo48Cq9+b&%=V$V#Q(^cubef#itiSwT(ot$N@fr1EE7{+sl^lDusZ1XJhkebi zar@0#yc$f*l1y$rCzJDhGr8ZYF!|)OuT0o4?;tnS zGLx&dCUPFwDRr7kqx!Y+eVKCJYj%~RYF^~`KeAZ6`#kR)n$4{eQ)H>#LTP)ypFI7d zgN$foCYObq$S;jea^h8;&d0%;I zotZp$NMG)L^M~uk){rmX)|B^l*Ob>ivAVtHCVwulqH!8OZh44PHYKpjx&%&aGD22W zb(c5NO(ZARk=+me~Z7QQ;8%vM8+LFiW$yy&2GSF8~R;^damIr=vhX*C>)i;y>&Q9U2rAfSE zNCK;z6WBVsyNpR~E^BORAl)9+k_}??WJB|s(mz!p_vGuzTL*u#QN0IT%9$MQm%>Z$ zB(Zv60_Rf#SLnLPiR+E!y~p}8Ai9nW9$ibe?pRZ{MX#F{=}DL5pS&gS0awpH&)2r5 zamSlU{HRX??=MVX%Li6+|3zcz_N8SY{Ka4W@U`vh zMP9l+jmJ+);h@V&{M7dlpKXlKtNRUQr-C}teqe2xJGZv{j;u5B*~Vz7p6mysxv&p7t|^9e<>-Hp@Y#$F`79vg^tfxwT}PXKm?yQBSr_(33|8fp77H zU!AVt5!!3)wDv6LT|B{KY|?pek901wu#j#?Tga*9fB9EhDNoP4&3*k#d1*-*TmC5F ztceeJ<(E=^;eU-gv^mM)fd_d`nwDRd?%?_i~ki8KwVAum0v;Fas|@~*rRUTs^( z-&#ImkIE9B9`=yEBJOcZ`wQH>GL>hv+{+XDM056q9o%qu136&(J5Fl(fRi@g~p4!N1T1a9)y(U_YUv)mBmNv9p(K|2e^6r-CUg*&E^hwInOzd z6NeSBp-m~Da(%{iwq6{cpvwg zaS1=aZt=AaIsBsONe+E-j9+~{$5lhJcy`C*?6Dw)U;THS$5v#q)v*jN*Bs(44`SH* z)fV1yGna#R=JE3TXW4gmI-h!_G!)9j3*~%%6tpd*QsO=Zn@$eZ=Ii12W15>y|ztdc&%Y`^X}C&r!Q&n4%$o6jlEy^z8K)kk^AuMF<&qT`sO z2l@CgEl+XRaNXbt?i_fWC%YxHpK%J;-+YYQ1)Sr=(dRg6#Bp9;JB61uJ<30CWw7(3 zV_fgZL7w(Bilc^VxJBy?Y`WtJ|EfyjEK253d79@=xWN5=&+$n2W9*c5gb!OC;~Z56 zCoMk8#?A-$URe~c8KU8hEjI9$n8TbqES~!qC-YCuX%02Gz?&YOW>Ds zhd!O)v}P%sQnr^e#u*PdpdkLkSB{|L7= zImUayUT&JgO$O~_;S$YBDH^s6+Q8oe6S={E`#DXf^6m3y`S*?oA~5|gS>9WepaNU@{p;gd3nDxysNK{bC(@q zN8ckHyDyz5bxP;%tx~xE@qOGpJ(_jTcVOMb4tJOG2DzR+CTO_**sbjIJ(8~l#Pa^? z82)u<3$O1T!U4zE^R*^1d^G+5_g}i7Q_jTjtAX2j!^5S#f7%9aF?b8l`J&;0c`>}r zCzh`{M)B{<>-lWr8g}m&!4V&Ku~Wic)@Ln$v5evaAGWaNhZX##PZ-bi+{8cPwsXFz zmRo1W^391mc-h1d{%~|1yKUdXJ6G@GT3@uhBQ}bgb=C0F&+GYc;c`yfyMenzZQ*gx zBKeSIEH~H`!&Tcg+;nOPuPt52PI23~aqC@d(j=M>#%efYQv{c2LRf$QDlXlzp8t&6 z!cKR0u}AeDre-m`sDA|4e7ug|eF)=z3$|hAJ9&oNR$g9X6YuG|fooN-;eL^;_&3zH>^JK4WJgehQKH6w2H_>k3L%AVb|Jxe=crch# zt;1PkxQ#om+`|js#jwfgEgZiqoJ;P9aT}L#Zf&-Wr)=BGyB2Ta{5>1E^^9OX(0L{M zwGLt9s*OC|eJ3w*kKrpnBKhOuE!=%wIQ!*?@fhcDwr;bHZMSaa)nIcdl$-bmbFCgL z`S#^?yu9UR_S(0TUm3)(u-VR@FT%OS+Hj6&9nPNG&0Gt$jPM$g?l9%q;!W-|0^SO@UtZW|6PTMwfkMgZNtIuXGZoiRJT|zlG zHJEFDS;9|TC1><=$M4_A^QSmpJ{LNdbu$<7ib;VSJj#dP{Wp~7cN)r#8&2czLzZ!H zdN8Y>g|PXR5U$(Gi3j!?$)mk}`K_fN&$zjWyE-o7xo&fK^<6KvDsW|=dmbE_HVa5<2xEavcX&#|mHHkAE0y0Tww zFP_|cDmMvSz>AwN<)ezFTu`xuZNGbPlXgB_b#pd~h<;v3B zgPZBb@aA_@*m~V8KGt^uf9<@0%^Dy>lou}wnZ_SC%*OZM0{Nr;TwYf_iQO_h*uK99 zw>dI~>pD&3=qFRyXw58cG}K)Jv6Fb;Gk;ceUdWU`8^1M<4 z9R6|)&)GMIyA7YnzU~wGMfnu2LgqdbXYq#BQ`k6g3fGIB$~U6}*m3o2zMDRc7r`^8 zt`|>vJ%&>=!1_((^7>PG+nXueZpADfy?+Yt44=ZP{S&xO#Z*4MA%MG$n9aWDC-H5a z2Pa7nUXnkCBhc&Wi4$4bXev9GP2okkQ~2T0DZKu{1a^Bpnx8K+;$sV1a>rQv!g70?hK3s6G?}K^Yn>M`3(31VC zyK?pEUOfD`1AmUT;j)7r*tWuquWmBqB|FXdg4TjZMt0$uz3q9<7{M=p4(8vx+HjxA zT{*0AFRtIS7ke$W;YHi6czKgHy!VY6M;|ofnSo~9Yo!Gj8CY{vXM3KpPw=i$LpbtY z2Y%q%m0M@n@Ww|re5uHaH{G(}Hz{VEm}kZfHD-Kew*@=B@4~i?t@-a_d-h2XoMJhU zYnxhfa*ho@>1D&pT{fKc$%^+@Sa5#489&}(#s<|EY;xOzcfRSueiyvf zdLZ`->&iFcY}mxuhL_sfu;;)I{C%z&H@IiPhYdQg>qiTAZDPgk0cB~>ta#BZE7onYW=^)}J@4&#Ma-c5F5nz;kCYfctL~}7r?7!j2YKJR?F2^yd>M2kHuT_lr7eL^Dq2<+4EeiJ!c@2jR>o%HxWtv(0(>9bpyf?tPKk=5pE3Y}a{fo-a3!OJQt8L8m? z$Lp~ARs&uc^OmMqR@0E0dfeDikKL;jye>|`ZM9X@CAgYe%&VqI&uV(9UrqBSD|m#v zg7ba!d1hV%wj2MJ{N`3svO${YSP7jrzkf)-rrk~celXVi}ZPk zzdp|jZ^%cgjQCXBYC3jG!P5^Z*!rY`XYZ+|VGh-FYyEff7_Y}K`s;B$%pr86K6hK9 z&zpBPI+3b`tuYRuJ<7o<>{1!dtRa5;F)pTUVcZwgZ$GS2-?&wpOGxqB9{SbYQ zjBCgq(G7V;rZH!nF=l0nf^%Ez@t!9NULI3Tu?gR)eAahb@KBFqrq$)h@pbw9u)6$6 z*N{__8*;+lhWzc4G2iXil3zE}<9y84;*o-%?)pxz7k;O<_w+bJU6*IRYREkvHRP3- z8*cSbbg)uj!2 zIM|D4z&zqf2pXQRjE|O zqBSjwXm;5x+IHp^Wod5F`USUW*o6|>a_kIE*>#5SU$N3(yF9A-C6{(?&Zn1)^XZUT z5xs0xM5~O7Xz-m|G%x8EO$fb3hi{Zn@0z7_Bm4|KY<`v|H^`&!_3~(GQa*XE%_oIj z5p8T=M4JtZsP>*x^1FD8*6zGTrEf|oyLl;%o>f6Nn&;6^N+rpq6uzmHs^*lEQOgHp_^X80ZmXa}7hlqiww1K#b1tPF$fqLfLh8vyw4hHB zJ&!G=Y2l^Rr|&De(ESx1RF~3)L8WvqzJdz3SCFu-q;&I2+WG{(lMAV$eIfnoT126D zN~v;PDMfdDMZ3GcB1fz-FDuCWY6Y3Z(E2O-$rPLv= zl%58a(q(u>cX&m*v`RYO<`v}{y`t%VU()$`m2|+bl4>>mLnn;>(5g;_bTYe?PN$br z-RM$^F@8Z6@T}ffNybMiX=3YF^wszk{aRN^d)HLbr!POrxyv64wEjZ|4Yg!eM@!z7 zF?8uk0?D%pq>WD?=e$&!wlj^cp3k7L6B#sfWg4BGlS*?{skGlAmD+VkCHt>2RPSjF zP5YccbHI9@Paw0$sq}Go8ht&UL6&J5bnHk587)hr7rv?V*(H^tx~Ebtn+%%&B!;q! z5@^k>1p09`fhN>ELN#xs(&+mc^z>8)bx+8k5BoAGbU_;3@=PVW0jadT`$;mh&Y(A4 z4$%4R1iI4r2zgvauB0>y-6u4WKtW>9hz@(`WgEQ!&HU1$Zuu&Na zRO3-98EsFaY|PZ+QU;Z+L7z*rscL-=MQ_NVNnl?t$fTd+GpL<9lP;-psD@nuy(-3B z&Zg3B%yeLC2G!b-LDQyXklwN!TDK;L9KjF3dSrekWzWc@dFmXB_0FMv0}AL(Mk=k} zpGx9j20hkfkp0pOn!G5RR4a2xZ&eP>SeHXCD{?4saSnBxn?rx6=g`hk1#|{la5$Bm zbtmcio(y`48M!aXph*90k_&M5@*JwWI*0D#j6|@j@N7Q#GQR@q1e zDYpAoI&HX>EPrX}@GA{z*F=%-zbG2!rlsk#qKNV|lzB=+8&Wj1AYMcLi$kgD*o_pd z+)6_`Z>4#Sw^FMw8tS_#iV7D+QRZMR4R_U&-P9=RejewY(2!x0hOU`MQR|#gnryz6 zewuEj1I@RRMa``=@wJ9_ZH}TB3!~`z04=2t(vp(2)Gk9qwbM29=#Ylqnnlr~)KE%k zw3Qar-AXI7q9{ZgMJ)rOXw=#$>I}c^zFIQrr=WqMq?lG&(ej)_6uyKSwRi0H=2;j{5c1l6!Y8_3f;sapqdmZ>c4JLoL*Hu-mrZo>iH3~u4y-ZD zm9A0b)K*Jt;^N5lAhbJyVxT(O1Pb1nK=rpJP}rsfn!G-NDvdVL?5YS_I4O#>Zc(&Z zi8HM6e)h*v`o1{&6_r3%F$tu`T5m@J<$_ggPN35f33R;HCepo+Aj2_HR6IC}O8Q38 zfv!iVM7}`>;>aj5j=F#y9GO6kz&8LtZF>Tl1O`x{UjS8Gte}cdL1d8~ME~s% zqQu}J+UFNUt$l(hm)Xje(4MNV1TZ1SrFo=e! zf@t1^Aj&@%Oo>avsO_>a(pwrv;Q?WkJui$p%nYNizF}0S_Dm{wTS&du2a)-rAbJ8O z4gJ_352nJzVA{JNjFx~o4JK)B82tzC<!ZDuT|X`%&U1KbjeXcZ_!(+8~Jf)(WCPaEF!! zQ=e5~^uafRY$iw0-ANJj4s6-j2(la#L5IwJsC}>>z1SQ;gXRR#*3ki!;TS-kpO#bA z;$T|vUodq=4$swLbP{Sb5!{3bnvQdNoEuA@500h!8^==4q_On0&sb{Tb}W6VJC-hd z^QHxFy(y`}o7|pylV7ama>PACHIbFX=VMfWKr!+ z3!j0z=S{mGc$3+EZ(3gLO_sO3$@7voUFq*mYhJog!NB44!E89S>pGSU4aZVz_#J-j zO%k_|T+6A9Bd`p_|~+5Bt#4bQfwK>OvcxTqt6$JFQ*lPO)daDLKiTZfm`1 z*AX8oIO#)=q3xMI6!z~r$%nFb`B3a$7aFp}h3ZHbip1q99xqYk|1)#*#wpOs{IUrD!9l=LlFNu|@3bZE4abVHTY#YIU?os^V_ zb+DU~dY9W$S(z=_KenZzzm-(`k&^CRQj&eFlJ*8FDF>Y0U@)wtWqp*?yswgKN+kv0 z3=f>KyT6hOAK223R(3R~o*i|qVMqJJl=OG1lJ=nHFSt|I@ad|g0a#=0lw_(zHvEbx zu{YL6Nhh1zQ4M`Ns{Cn7f8N^C3s)t*@2sS>-b!j^r6h+|O8VGNNp*1jtecV)w%~gx zDY&DOY8cv)Rc$+3SY=BA6}Gg>K}l*OC2eT~-b6_|n3yj!&AV?)r%b`uQ_?U!C8g9-(g_14^}`u0S}N%^a@4~*$yiCN zEjm;DFIzhQ+LoS_*i!p@wp3b2Ntsm+^y7yE?fLCM)!+*nD#-x;IZc)LOsu5)jqyHy z+tS=`w&e8EmZ~s2x^7E+(UyjPcA&g(4s@Z~fo}Y9pym2XDnpj`jo=G)YoMg*KRVID z_qNpRnJwwTGpqm^@@z?wYfIKSwzTfM1NDM;Y)vISM4l;NKA;DCDCkWm@SHQ`*wkiu~A$Y7excreNC*wW3bo3;(mC8OYni&x#h0wW9y97pSx%XLt#4 z?M9nYS|2Nl6IRr!KQcR8QHhHcU4R}S?}m9+WH8-|UW~G$Ysi(^+lr>zqt4cfd~m(R z&War1eFV9$4X~nWCo8H09T?e}wjpo81S{Gx+=}Mn+zk#mtCtmR?`cJYtgUE2t~S0A)!6rjj!45}`*nU=|K;OTi7RaEQ zW<@opTG8HdR~1RC6*@n^O{aV<;lloKlj^DfE~*tpwwcz8%0mp8#KE4#atf zQS%;>sDE_MoGRe^kNu)ECmr&h1Dgr9)BkZ9VBPkcQ+GUXiaHzUA{h59b9_cKr-$&) z0DB9*t&rI^1sTVoH{|slZBAno%;_l3TY-5r1ZQ^MoSYy#@JDc84>LG?!kh%WJA(K2 zHm7*7o?h4o>yJ6tK;A;sM#8TEN`l@Y$2dGM#krT^jc-!WJTQC4n$z+z<}?~?7o7JQ z=bIyA4m1&Ge8k!t>;XJiBX=Uar(>Qy;>>9y@|qqnCwt6zK$u?J&n<9jcy2?lr{Plh zb@(!@0BJy9WFm)Q>^dY_Nx*)_CTG zz5A${37?+-e6i|~c?tR`g|E#rOWK8+r>H&f&ma6k$P3r{ct)7VTQDlU`LgdScDTeGJyh6jRbqH>C%$rnGIZDP06t4|^w2w-|dXpoWluE<;D50sptEj+oMv z{3nX)KOEiz+P|20{45NGRPkq0`@xA|KMqO68XRwCz#SaWH|>e z3(xyveE9YM`dTyH?#KW-fE zhoBc@tVK{X_Nq_77kfI~KY(u&Je!eWO6BNfG(1=0c@t<5xFVcC3N`0({qM}%@R*8> zufY84b2aWCpFw7<4Y7X;=bi_<1@&3r%&=Bry^rTRz_mecGx#5bSG`l{8`tsRIzmaP zaR&Pt^>v|ZN&oBb-^@cp61K^S{*C$x3k$)og zuAVlfVEFaK8S}B`f>(q8g>#={zbn`gsPzK(`5@i}qz}Fop4XB_lrX>upB0Q~$Pgp? zJIshyj4&e2Xd`Mf)`)^87}3f}Ml{pMh<2)tD9_i3+D|c}8PkmD<3J;F0GH`%L>X>I zr1UVN;iHTw%-e|ej5nfyiAH1qW&@bNP}XE4dJ4A23?qD>$%v8%8&OkmoxmLj_hyU{ z-5zH|r{U=WztvzKLzU1vaK_V(=mFSH?nb0V&bcZh`s`^$Q^BP}ZIE*_WIgO|WQps^*mJ`3fmm1LnLj-K%|8pz z?C|UxYS!VtAu?Tt(r`Tj&u(Jh7w2!r`6XC?;l4h+|Dxt9)-PDk;H*8!5)MV-zB!&x z!kONvACGrd2Wk%;!5JDn_nTxup1uZjY?=X?%ru~q*#^`qzNm<2He*v2Gk4OC2$kx8_>}}1NyeifXY`G(Eim1G;^&1 zeF0|=ZcUg0eFS?r+<;C^Hz0Q?$=`tLfO7>maS5IW!5f~I;9^1yXco9+C=6UTa6u8M zpJhNl;dx`O0bNE`e`LK1HW7YT!M%b;f^mgkEvOKz2l7UN9gdvG!MT8|yU2isB5N9S z2L1JgUn028V4R@l;M}1gup7Z{N6zX61~eXPG*~nE`Gb?_?+n-kFfC9&6zgip1lJQc z7*HT`|5|83&d6C8+77=WFyoNZ5Zpo3?MK}v=r69TAZsWF_nu&Pp|?sfHSj)8V>a#K znF;lS$0gKQ{>z6o8H@v-ZGs-*dL^E#!2E)yuS9QnkA2}=AA7g4=Ly{dw+HuzV4fjM zH2VJvox`*DV8)?GKji$5y>dMJ2<8QJ8G48N&frY2HwWi-h5lVX0I&L0gLXjSe``>> zo`SS>6!fCLf|3jsl-5*1{Y@0)+Dbu>q3fm!`meQugc&OZY~^^DkH{ zm6bx#xKdQYx4u!OIM}LEJhP}29lBNu9nRf_ng}rK2Ud!1kWK$eVGEf+4X9F_LDo0$ zJ#CD8cuwzFDK>z;A}Ynv0hOW`)Xu3=bb^v_{Q>F@_7nWC!&3=}tpYc_i2?=-C1M0c4(IQz?d^7bCEDQR5138RP)Y7r7bF zu7Jbeq!7{i@Mu&iUSn2St&tmjMqozkka-pAUxG`6hQfC-?k6Gd7O)GznuE;;+ZmZ# zv_~ISct3bIb1Ksm7r9eWGXNTby85_}MnAu8 zD}^c69+303a&hHmx!8HPTpTVb7ln_?h3oTjvF}Z}xbvx8q*axR@E_$O?|ZqxKZ+w1 zU(3b2kL5y#{rFquVoG7Tn0vom%m=sgX}PF=RW6=`+Xt@I&vLQYRxb_IL%OSzZ~w&sm;;Sbgk{qB8OE<7vB zMJzI#!Q&s74TXVI;(iA>1N7VLbGb13fEg5^XUr?-D*V9jdR;Cipx1uL^#HYLV3hC+ z1~Utc3HHXL*SDw}aJgI@10M~3GkPz^vx&%TflOP$t1+8R;Lcz^XYuUJ+j4OUHH*+& zYA$labJ+EA(e7Tk$bD2UR+N{E7vL4}2!MQ$F9uA;%X0AyJvrkY?m__ z2Xpf)DHqmwPv6k<0i3@S8Up6WvvTpb40C}t;QSKI=I%w@gViDPHn3B`8a~0h#<`18 z-wfx}M15Xqx#)}fVC;M0$3lB#T-m!wbm&_oJ`N}n_Ctz94fi4uy}hc##NmMGd$HTqKH}i$u<_BC*?}NJNgtIpd4Olu1QmqEC_V zhaTXJW8OvL!pI_F2Da_cBH`G-NOYA&;u6?pZvQ*?6g)lPl?$eeZ;_a(#va&eJZ}zm zstP%g@e#82a71pfm;RjvUqj?dgr@*khBFk9&sdxRt^`~f>M~IO6kIU84V;R^;K4;= z7WzE~okf2e;N^wv{gJf^I5%)P=(Dp6{NNc4_AWAy?pGv^;*8taKMmjMqtH7v6KXR8 zGr%k+;p~cmMPdx<-(pT3F{eSeekPC?=Tu`3>rrUrCk%NlFWt_iVStNSdq231hyB7&d+nuG ze}x=oV1Jdgn2-k^^@j9_NLnjRKbYe}RPRuOU3H^II z;d@snUfKzDFmzZqcnr<4c4LG%gBv0Eb;6GdbCHjf}J|y6{-`Lmg_|5 ze4TJUg=fg?k)#uokLbh=SSTt29E{xTQ`|$XK zx*W`)Bl6dWmkE4MCFsQZ1DH87ZUASzQYS*-MU!-5{0O`c7o9kUylV6k0d^ahAov%? zfP>m0W6lQ50U5W>)`^%hekybW{3@%p)98D_9NRwsU8?%R>?A9HDoPV@-DJc4w>VIF=g`0B(^ z51lX?fcI;o6XV+BowmUJ^h6OnCsEihO%yv;CyJ_&L=n3oQM`dn)+dTNxL>^{QA7nL zir=VdvKZHaiDEQ#2%Oi#L=nFtQOsPIC@R7dMQd=@LiQC0+%O>`AZT- zA!>8xCyKSu1)L*sUMM{EQ5yiSe5@UzAaH%aodxFsZc9L-2%ncI{)1n_tVH1hzn9RG zwTYrG>JyQ1GR{%rz7e>uV9p}nEBHL}PZW3j5=EV9i6U=uqKH5*Z~oPRyN}E+xb}k2 zbe#7BoEN-iz-J_w@~Mg9oll~;Jt0vTj6)7EAE6mIqc4;U-`6V>g*mt<$d`$1e(<>k zCTcw9;FTzjc_xatszk92{msW*65#0sRtt8)vP98rL89n9J5gK$cN5%1_@s=+`v6lm zG*KKJoG502y###%dl$1AgmUtORDAtOJg<4?`-ycv| zu2wjrCmlSDz_dYK`*>vAuNAH1v|`#`s)wg(>PcYvH*IJwwaE#g%G>|9!0xcd&;!EkK_WF#m;uOp)0omZWX17)-;FTMx6+5=$Jwo^4)e9Pg zSzW=5FQC_{@U4&8{$snqR}J6xd$eK{e0#w6`VPz+x)!1lM>lB13ylWPHDa$;BOb+S z(3?iA-lGv-xIY%H5$|?tMAZ(BSd6tCbsO>gV7Nw@ZPAEpks48k`%d8g>HcnwDBYzI zZ}EH`>aT!13&rEw8|+}z`r!UJ?q{HOz&?%G4^`m$F1WFHUbRgl>>+(i&w z@ihtO9@dC{SiNz^n;9BmjJ+G48gb86BZ>!WL|v@b!!@FXheli;r4j#)h2Ig4_?@N^ zyHYg5C|M)AqW9~I;fZWNhiOEJ6J|0{BQ%3FViC^JAF2_(+%zH`XOy4Nh}=^ek#-z& zh|!38$m+95BW#eX97o9btCA{s1Xv8e=7RazOLnEexoqZI~kw0j&M*Nwt z5urXBk%b<6qdo_`GZxyI+XtCPGAAtQy>(vTmk=?tcZle&3=!wCzXIG>To39KBGy2)aXkRf@PCuQI|~u3JVQhW zFyE}OkF(q0c{0vC$RXkm_{s1Y2*1H#d-e?xjOSVR14O&B05Pv3K(xVGtsQaZ&=B#? zJw$wP4Z&y15V03^f!71X$NT^x3j)NMn*m~OQGlrPAV63@2@rN*+Yb*B#@-=f$)pf` z9S;$1BSVDlPJoC*-Oy_RLU$!V`~mv`?AYP};RUu8eCvZ%P7M*GXQSt7A;NMB`u!Xr zT0RR9dEkq{)Pow{2oN5(!60vJX@FP^)*5U;AbMC4A}W`Ji1$AOM4w*);_UMP;RcUw zg#lvItpL#s=WHnn5dO$|qC7yH2Uq<%K)hWUBC;DT6#DfSijB1w3Ztq3F$cay$Yuuq z<=p`B63pPz-ChP#gw3q}D>w3v9+4usHt%_^S^C#6fTe zUjzsvFvVaReG3rD$oN)&p_mQs2hND8xlmYs4-ltN^Ykg^3#JxU8+hqH1c=9A8ltB! zH5Q78bruTGaJBH;q{d%YREuTNYH@RqS|rD-#f(F05t*VEy^pFzewtborK`n=lWH;N zj9Lu3pcc(8tHn}{T3pKH;F$dMcEmDW47Er*s}@5p zs)Yr(X*bovBt|XHL(AZG^srjohEEz)>zG>1hy37`by_XTqYxK3BpV#O`BxO_(~ zwu3pAs1^-zes`T()I6mYA8~CD9~;OQ_shU7%ux$H_PHd=r+zl9~Ia)L#-9OF6Dy3z7h87z-v~y zTD*UbJg?F3J7fj70%rw4zpy_S_uarAM#j(rwOCN77NsS47f+Gv71j@Gaqf#+T(4G( z$>@0mvNu8wzf!fxyo>jX`ZQ!%igi8q-~97?1CP(>{|DaDU$q!r%U3*uS4GsKp5|G4*^!7eims#9t*kL4D__#Dcjhv24Cd99X0h zrT?kKywxgkI#eZ&MyN!sZMcqBiLrZCB0fPSz8zADo-;Qqc8}XLzNisf4diB~G1E3Fp}=k&L|I@Jj;sIanq9)~UoRjY`-> zAs_s{fGdKRJf;#k87k5LtV%S-J=oSPH)cyuvyxQ6=0l%gRiZNY7J=5w}!g2biX%$Wx{gFUzrq`A1_OC*YHI z05kvBALe=S-#jsw9XYtZfwSS$S1M8URwe!--!Q!E1mv5BS#~>u{@^!9 zhq-6}ueVX)B)km4Xy4=Ae#SffhForrBG=VX{2A;hT01+6gM%E!VHZbyUT_p~BOS%= ziH@SZpQCuUz)_?xcND6%j$-b5)NgPUX#*XF8U7i3r~Zzj6lx82^AJbjI>J$ujdK*! zrr{a5GH~bD;U8)YcNA`$9mT>ej^blqNAbwcQG8Q6;`>;R;kU6mD}AOClXb;toeKq?e-@W$h@8`#6e?>?m#^_iFUvHpx+}n&l{l z2Re%FYaE3KIOpy7=gfCGii#LV5s1~fo1-`ecASHwD2DF}^ghJHQD~<)3di|4E67n) zqQBYjy8#CO>lm?npQBidHK;qz=7Ru1w?EG>>vi%3D2<;Yls4N4pC82Fq!$D-}U}+ zuKS7mb3Sp{#&!|RS+$JwU~dDrc!(!=v+pckvpvMOnI7UdrH5F?p0k*f|9rmqGsr`n z!9M>*au&?Fa}{f6f0c|gXg$Q>d=F9D=OMP?)sk_^oOK0fJDWL;CvxV~Jj7Q2^U3qD z-{2vx*uk0P@O8$tIOHKdIm|e&u{@I_ULNA=(H>$U&qQFKr)G1HrJOBup3Y!TtnWVa zTxMLO;~wHyF1r&RqUi|s8Q>v8u!s1LXBo*eDT`rkJd-HSCxbINkmn)(VBBNIoGsvM zl80E!HO@vU{?}0{y0=$~?OQ3u@6DBBfLzHx;-VBkbXAH|oRs34{z`G6uTtDGUMZ?V zl;WOgO0kYByoFMXHCKw~OqHUUiBh!X)zVTaRx`Gfol<=0rW7NGC`D_3rMP^uQfw8* zT;WR5it9psr6~L5DE{}`QOy14DB3qxiZ`1oMIURWIID+J9Pyu09PgtP2TxFnfm4*? zNfqClp%mkIU0CZVUi`pVUOUw}iu}90;zQ>4>dahyl;Ru@rTAxrQfxR$DgI@h2P2ds zF!mB-OZAT8kyno5@b`}5^DmBKFk`QnDaB!3l;S!UrC2#gDV`dm6iZm|BG#GD*m%Z< zF;@A+QQY~$QT$oqC}uFXzf38fZmkp_bXSVo-8rXWyl)Jv z_100`!kjnjDa93x?a+aHvse8<&SA@MTIRRryo)Lw#ls&RMfp!h zF|!HJkY{XerxbUiQXDi~DK=xh|MQx`*t5L9BjeWadY{jyd~y^|7#ziYjg(?*{<>() z`8q3=Vgl#8+>iD0Y*w?^BfNJRW8%3AIB#n{@A*g}X5LeXAMYx}vBe7U*G+}E@U}us zxUUeiD->eRXN9=e$VLooY9sbm$ zltS#m9(KG^h&>tGsF98M$kIkkX3h2;ZNvqvJA&89H45?UT7}pyO(A+Q{x45}-({ylj4V`$VVqU32MSSmuMkff z_!?t_Ol?Fj=C0$syK;Ty^W23B(UW--HY&to_B)lcdS9dvXFXAf%c~XQU&aR2w-JXo zu@Q&LY(!VC-;Hg=Htb`<5{39+r9!-r!L!ZdY&oZ0Jl{Odc`{!bxZ;~Cp= zj}N?V;`1qg75q7(LOiyPb#dNjIpbKK^(M~gCS&aw+nlp{_LKGhX5UnZduw48zNiKfpFSj?5~Di`hg z$i*=(a`79mMo;GTk&6cyTh7>yi@5hnzPCv(_S(*TtlKtQF5cxj)J4wkd*z~e5ANYC z7s*2|F7}d(?HT)YCUY{jG)XQdtz$jRJ!_j>{IWwXzT|q`nmMfGqGM;dc%qkF+|9fv z*mvq=xo8y0xy8uE&dcSZ^(wjO&v=uKa`6Gz1upZZa#78AuTIRz*j0nr+X%TR=e#Rt z$i;v2R5LJI5RY9nJmEm${W9hGuX;lbzL%oijV8}OXgN6E#$u{;CL zY~xbSkNfoJ+RW9kkzA~{kc($J%EheSoK;^rzxK$*kAZS=#56g7uPPU}%w=6X>;LA- z#agZrT=}f$R|EEM$zLCxJR%d1osx+qSDEV$_kS!Ciz{TJ^?R8(v_>Yb ztd)s2TV&#dP0YKQuVu(Y%N;VY{a%@9c337ZWj}pc_ts*WIPrl@T+O=sGWWSE#&C~^ z$?S{k=_;8xaGgw4Fus=YKlk%F`}VpZ6I5J zA`_c!WbR!uaSL+`%HjK9kK!(!S0a+&DLzK<~X zO3rL{KI`S&*6`f#vxe`SeFgj2#(muV`TAs;7{lC}c`~tEJmZ)!tOf=>UFLIB`2?jhEZ@|M`iAxPww8Vhxml-f^l>xKU3^*#wfa8xE(CnN6 z6K)ys?|lOZw>f3#(>i_22@2EaK&r`UY%vYA$-1yUVDdWFwUcw%Z@>qP-*eW0b1oZjw6y^{ za?NhX`E)hlV3E0pa9(3r{}cm`h&JH%6$Tu+(SUb%88C?XUmWHh1qQrg#Cw_;Fxb?9 zcCFc`(tv)fWy@d#+D~9jtUY_70l%(fznpva4g;F!81V7|1NM7YhkL%%;iunqxYXQ$ zM(qu_xtjswS^qBf-<$H&Li1?HJqLGw*Q~q=KFQn>17>u{8Wb*{?=hKG6G?9`FCu=bT-2ZfCz(`WwPgiPZ&!f_ z6&3j2t^(UTRba4Kfx`w@V7Cz!_@hRT4?pVBzgCY+xLiK#@gVnF@mr5yxJNy+3N&g} zf!{h-po4t{E^w~Ej(sa|{;&!hRHer`FS+!3Y*V4fW3TkM{k0xfa=($^^*F3v1;&_G zV0XsHcdEer9u-*TUV)n#|#|qqm6&Q3*k3Uc8(dx7wo1M|)%!_(FeMyf;7;p1jkNNM|(+}3sumTS^ zuRyO371+t40)1R7u!B~Q@3Qo`cNcT#=+U!4kB_;B<5fMTvW^)q^f>#I9t$|bDE5A+ zZ3UJycZ#wCyR2c(b$Z;omi?vaad^HSM;u@cg?cQ%rpJ~K^(bfV=RAW(?0abI3hdE^ zGwWS}e;4Sn`(i!ziPvN6R6SnZq{lnkd1d@s_Oq3_Mb7US=jPJ50{^sROy>&p?OB0m z!}a_a)?=DR&(9fp^jOK*boQF7$1>J$b5oC|?E4+huQAW;6?-1Vp0jw~mXq|jd#WB+ z&(z}%*05kXdtcAT?El9R_Hjjz){pe)R;|au|5!Wc{f9kQvF=;fb+}ol!^dShbb74A z121)Wv`UA8H9B1QO^026>M-c94ky$v#`ng>Sl6N$!#Wq^RAn)aVyxRu9a`Mg;h1|m zJo8Y8v!Ckl2lp6Lsl!!ob=c&i4qt!OVKeU0+@u&sv?<0F_QiPjiViPa)ZvyQ9e%o@ z!_lmvjH~934nMG#o80T(3mpoqW#<+siea~KF&F%!^?cU z^^y+TT+`u5*5p~PLt^}=4?3LxM~4ATi}6jTVpQkq@O_RB7ie`D&hhwpg3dQ6Ai zxK}D`YErC26V5{YUWZ=4bXZ_sjLSL}oq#8Sg*sTJ9T(+w+`oK>+t#k z*1>o+XEK_#cVX=H-#To;yl#wrvP_4WT)uHS?7?d_wXXtTSV+GIf~9yu&zmwVw0ld7o)gjKk!`xQ4Nh zC+jdFggL`>m>j7?OU62~_F&e%g?Z<5<~MJ%wpXm-2lr#_Q07hNoSQc${ANnHyA`2d zXToX+!Y8hTt@{zi3?xh$O4!_+FxQW8={UlQP{LhvdEW}cW=#mI%m^LY@P5X6*b-Xy zB7D<_Q0+n(B<^B5$o{wZPoKN23}D)}ej)87($a1LKg2nVvBN7mf42kYSsHZXQj zIAMRrcCMB1@+S!|R!QjdK|=E`~(uL5GvrhFRd>_i*qX^e>&i(E&$6X0;a2+p~Fzk_p z{;wpw#a{lfW?#-Mif2BtKWp_T{5+koGMZ<(jPQPmghsa|w7w%@H^%N|?&)tNq^}YN z^Zcx>2&ebqT!wINJli`wpW1nZLm3;mSBpFIv>1F+i{~zB@k@yoHBYrT`n4A4zSrXP zk6P?sqs5oCeEo+O_teWpzoxlZ(J>cqW@|C`pcWNpw7B{@-z(Lk*<&p_zR;qHUW@Cv z?!D6D@3&gq%ssq+YjMA6E?#Hs?S0(yh!(e>)nW_A#xZXwbn4WDSy zlzS{;9l<}f_}nZPZ93)R%0qnbxE5WQ_Y!N~T%^V2SGCwgr^T+t|6ixdv}p58i{GlW zSj5;@jIG}}7ylgAVxJRQ43xAu^OP2!aE}7gqW*#weJ}IAYg$}-M~fDm!HW-C9Pvks z11xg!jUpF^AJgJiE@IEmxKB)>7IzhB(Vy4pr?uGbiWZF-oAg|Z7uoOh`nmX^1!vVI z7mXN~&VHTtvo7}jzjGb;*~`}sF~242?^>?KMV$N3I?l>C7t7k_VxBGM&3mpK(4vy( zG?~jUSBth>!&yTRdzi)fB|p-lXSEhD|JCBHX1Qp}8AtWV#ZJs&%6q%Zgbz4T0HxOdo{|%kMdl6#a>f;=c0AK7VUZe=1eW#=Ze&_e(o2`p0Bdk z(LAe?x6IA?ZsfUsVZEEW<>Kx>x!81R8ctr3hFjOC;k%t_*kFGeJ}F4UJ?GM}+od!# zy_|;Ycs+eF4If`gL%A*ulOJ-uV@zTimaa`hE#t25OT%#|({TQoG>p5DhG|?!xQ7+j z`15J#%h;B;d40-#YSOUpM&{U>hQo5xaQm?|RGmu0AB;W5`>S|uc{&XzaM|(sUB+&? zn}!!F((vea?zuY+jdNJb;WTW=S`SegUM)<+c;27D$1V8UEh!Ci8Jm5LIqs)npSNlF z?N=I_vA=(e^E;7--&yZd-h1U}8k+O*&f{s=hOcj7Y!>TJFHXZ&toO#pH0)=Tj=h+} zpX(#f%1Yu}jvGwWSfmWFd*@%681XxTU&H?fw3tZxMOZ+9dO zyE9icXI4Ky4UHIkjn8lMo=VR57jw3*PQwm=(okuZj-5Dr8Dq!s9xv8@oG~x6**9l* zma(H*(^2NkDCb-&+1s}?9N91(KUk*Y8Sc5Ay}e+Kw;0z_%N$%LoWmT}KKN`J{=CJr zW=WJWX?7X(s6RL zbiC9q9pe%bareqZJeZz{^R>`VN4D4zWtw0_V(iv`#R72 zFC^lSYpj9$Zez~;uk1;dgl(*oQ1LR6U(XWJhW#z(-f5g^i;JB5=|o&`CK227@i^A- zKhBFE5;5~{B6ez?gf(4~Fs>pI;R$EYyc^h)hWDCs-U`lcGULbay#AM2FXn8`ob8$< zp{i{XdfO-A_h+09=W~>CcUh;z`%4+Ok?}jvG6&~-f;}~n} zM{2NWwgwxo)Sx^~gN@QP_-d5~ae)RqhiXvgr@>i6H0a}_L1RAmT&BVI%&F_A!FkL% zjQbZRYH((X22U;5pzk~lzL}!IMai(f)~V6p z{um8@rrNIZBeE@S`W&IVLY1=~@Onsoi z;y=9KIvSnaqH$k5&y;5)a(*-ZmnHBloiunx&|oI}{WMd9@0j!KMV{9?4c;-0##ddV zaU?|JSmv|~*WfzVYviTDoTf5vh~Ls-kk6`b>a4OZXP;DB!$>}?s1gS$oJC6{Pa zH;F*=Z)yzvrN(3C5jb2Cfk}=LSl%-NZ?=lSH`QwNEL3C5#cI4YRE-&3)TnE##-a9V z99*Erwvrk{ZmZGfjT%dSsBsuqu3n7-V=gi#sjnK}Hc(^CLls^vQeoRt72f)-Liczz z2BxSnZLb;~PpEMj_c@TGMz3Yu(?^XOb2Xm4qe9 ztj|`B%}msI94|9P1)D0aNK?? z977mq^dcNREqNZ}RQMvF@73o_#;Nf{mKryIP-9q}3iF4laB(}%?|V3IdKr$hpNHeU z+HgGCUWM%gRM>N`3Za&DajtDwsnNAqjlE4HaP)3J42tx_m1F$yzz9FA7wCrzRDO7L zrXLOu^~1{p{7`4*3|C5P zKdk=fi_eevqN~~$FIoHIhH@YLywwN4M)}~|={_jD_r*~UeNpqw7i%kh@nnTB=HK$g zD|>x0Im8#OEqt*N`QWo~A6(wm2PYf6al~71H2>m_-4FYsHq#ed^4=L4zSv}`FFy0( zYVM0B=Y4Q!un#sh_QA^Y-dMWE8*O5}F)+a!+vIv<@H}5!In5Wxj_^hAUcT7ppAUKz z`ryVHKKQq(4~{tIjgzK(<3=ZMEbicq*+| z2hX(g!B1zrF(cF)C$#d$7cafA;Vm!h`HXz44*7H(n_9!kxRku-hsxJh85hjHJcN7e;K(~J znDCN&Gpw4eK}a>0QiE|}%wg6Y*RSa#b5@v#fa z8@QrDaK%nDUGec;7mSZ{!DlfpIC`23z8UC(pW3?M^>@yAQgX(m6lc6x>4HfWE?D!& z1rKy_#U@^^SQ_GrTYX)yz}*EKHFd$kPo43!)){Bda>ht-#%9LOXvWt9zq??|`mT7t zl`GoXyQ067EBbbI#R&~v@!~BPq$C&Y?BRlce=+WyGj2f-?^AbjFS?o$>rvC;n`~34d2P;atv4ta8B< z%<&}41((ip!Tt&t{!G*vd#jysa&u>#q;o>M-CQY7crDQh3pY8T`*9~6$@y*G?1Cwi zT+q$h1?LqzV{Vi)Hf-UHnv+iGKf?(H(FtdUdE)zUPyTGn6a9{PVz(Mk?9Z5Wtz0ml zdDQb*S0`tDf7J=MU-!i08c$RSo;cRZ6Zb1TF?*mV#xC;2$rn6vo#jAWUG9u4Vw|yj z@jyJ(c_20`_C!sbC(c7pyldu(9lj4h`aJ-@cJjoEAW!_7?TNSRJn^sJK%8`?9VXkh zL!;$waYJ1j>~GN>!%wxr{Wfj!{++gXHm4n)Sl%9&`dQ)hIxEbMw#Ioktnt)8YrKu^ zFy77%e&+Fp0;@4lr7GE)DEK;w#Sprt*~mF751^P#;z)BoRVqH?~AQ* z)kSM;9Bzv;S6iGm*cP8{w#Dmh>@f9^9iGy*N6Ri&cqGLN>nf~J(Zd=YCRyW`1=jd{ zp*7Y8_CVckTYPlf7VZDq;-x4%>}zC?De?B`|I;3Oj&MMSbq?6?q61!hjy-m|YL6$|IiMN$>pRN61P7(AuMQggw^u zx5rn#?eUM2^$oVi+4JnN72k7tVUKxo2fW1llX^Sgvw9Agb=V&BN801!?{;{H^ZSuv zhl$B{IB=I8-Yd34(`BqR&7R*&+M~y1d(6FLkIQ!3WB>8?X!O$#%h%bVt+O3Ef3?L1 z&usBuwJqwc?9h6e9jP~{-4VTVaB-6RE9=UV|;MB5uWPV2n!cA!n%o#(RoV~ z{tQuuKWt3UwyP-~d1H!^^UP3o*aVjr7~|$787>;t1dII}6ekK~1pwbz?mAr7zu=!=?$!#x}vQ&rR_1e)c`x7*m^? z;M~0?SmS7lr!Sad=4dl)dEX4Ma z#py~jv~n}UB^}K$-qH*$I+@{1A2VFJ+6+6Fm|??{O;GD4LrX_v3>spBy78vy-kY^) z&9Ez(;lwyIthF)2(|1jA@jg@Rd(0HY$DDN=GyFVGhVquic(I`g8kw1*@~0{O+hc}p zD$P*3VunGibKZ4R+&0t{9qUXm{+S6j_-=x!y-e}<*J{|^x)uh6eu0q|U!mi_&u~4p z2I8O^o|aU>toCofc2OnN*S~`4%_<@2OeMJQdkvMl-#|qA7wFab8~mRA4aN`s3TgLi zVfDI?(5>lvXwc*>3~lflRx;-H+gFe_s1lacdkufzy@r}MZ{XU??{NLcci3O_4b)S= zfKj&^NK5zt+U`|QmGc^|^sR*6r(c1^{a3Ins1n}xeGMi4Z=l`cx3IWg9dxVv4jZ3- zh1AEj@NvRND8K&>Y-;%a-%3cUdj<9Pzk;MEuV8mfB_t-kh8MTqfV^WBq^|n`Pjug4 zyYL14o7KRaJ0D={t15VLkbk4s<26{tRl*6UO8Dtr32pV2aJlL=yvDciJ*5h6I(~-$ z`B!)rS_=xzNAN6v4@b^bfz{Zz@Nvl-sQmI8Ze+X$uZ6Fn=jGR6m-q(iZ{vGX70g}o z6&?kChM>2fpm}UHq@}-yJ-+W?V__9|#8p8 zD$sC`KKE*&XM~Imv z2cCob^0RRM_F4EFd=vhJ-GY(_I*1Fq4P{$PV3^e%sJ&YPD^A~riF|#4(@n^!cMW{C zm!bEE3!qzd9v)mi58po&!>4{FFk<^1c$9q?R&*ri~}DtuR70h=9{0r!l~aN;yQAm4Q@U3Ik5xg$~2G z_ln!FG)f0CE;pfr`E{_GSOlNuUxjCf%V3>Y4)1oC!-Om45Wlw^ZucyQ>k(zpM_md= zc6Y&Me+kgXVmSTp7SuPn31j@PL%#Dh$TTa5W`X6fA-x>34wS>e?d7mya5;27Sq9lv zrC{>>E=<~e2Ts_RKwd&Ij5u`*UNpT456@qRAwSC?L0%4<|0@TJ$>s1kv>bLR%c1@4 zGI-sy3~EM|LL;xc(Ax42bk4jDMoo*sDE1cgYkLzqZY~3-^<{93wU<9F1KYo4Q2o6O z>W-8_Z_X@nPAN>?coz&QcVK@&3AF!L4F2nNPzkqS#LrUL^r95H|0;!fu4Rxrw+z;< zCW!s@lz7srG>;nD1-7q70 z4_tEF3yL^K;>eIv4WxY2n+CZLo2|4p{$UC(I7m z4STomfx8Fx!tgzr&@CYgHc!h2`K%n+yGIM9PjVsWTpqk?k`F_3^MEdGgKv#?z|M@F zkOjM;)#*Jj#yArm+hjph%WN2I$cDRy99Y#W7m60=!Sa>)5M{IvOrPgNj|tl$J9!6W zneBp-+})7cb1xi@&4kN2S#WG`HfNLr%@eiIVP7sdzs-Y}-|`_oVIMSFz7OtI?0|$u zyTB}PH@uSefUI#QSSYeU>Xi+HI_E&01>=l!;dPrlsC3MSQ&#)HLc0%cAKM2_dhCW6 z#U7Z`X)jFfnhBn5vS9f0ESQ~{4X7#|jy*aS#O*T{y&4#%q*)Y^68v+{Tz>yz0aOHs(zQ^anhT2?sX`2Ue zTt9>JV2)WH6tXAH7%l8)uFRiVaK$$ZjJU49$b!{3v!Va79GEao3+Cloh%?WH(3ZKd zXzg?$(`oR0VJL{s!O%-R36eTagr(zyz@t$RTxlBwt&)QvV!=e1-+3}@8W{q%zox*w z?$e>=e_U2mVaU^9*#CSYzm5gM{rBTxZOid6wIBepw*|l=Z2(OB6aaUM#zUxAAPihR z5mxLC27mWy5dAg;n(UkeW3++b95fytm5zgW%W=^5$XGZMKNhkkjfMAp#=^KMW5K51 zI2ff5fN|l0uyR5uRE1225ph9aZae`jAppKG?qBX$h@3taqPvZSr?32BK)gRB4)h1} z#s08CH5Qt54}kfjg5g1{NpS8}Ak^7{mArHUTizH~@NH8V7>{#=*)fW1(yq z^S2la-NMI0l+QSLqzeWw)nvF)I}!eU4uW$dgJ5rEAo$%3gjRb3;naVDaBjy0=vh1- zZZ;neKiGfHV(xz^0D>n@g(ml>z}!w#VDPq3nA|rM>h6U=bZQ7xv1z0ORSk(d)Uace8v4IiL7y2aczit^LT^ro@MU4p+hQ6V z-!lc81%!ghoDk5w2!`d6!SFnGE?lac3y$@p;BUcPXe-VI)yg?=DrPp^`Y{tCx<|rk zmk4+jtODcG>G1kS7*soi!RoWq!0lu#1pK!U@|_k!=(SkTbcluZU1MO@@%eE1c{I2d zM#0b-b6|bHS+H?QBx{=idyYrIyvY&JVvZAt?VO<3E+vc*dc%$pJz(T`JJ!$*Tw=QL z<4*yDgig@znKjsrY7a+tw1J!=IRp)A0q*|2p!Qu)@bl>b|6R9(N$0KzoRP-Ue=m$l=3GOPIUK9BemthbtGl!Mq1u;Lu1 z23diyq&?g)X%CCl?ZBj7TiCLr6@2d30&3edgDD40U{p*e7;4uMqTX9U!}0Av*R&02 z<6A+e*K+vtRStW`wSw54tzfZLD_F9wCH$(jgy^|VVdrQQSZ3A^vcube&2RRwrUgu0 zX9+5`1vpNzfOQs@aH>mlIH+g=d9f{EW_5EoH^&mrST%!(P0e7mxf~*Uw*af#7O+a! z6dZe-!?+MLeqJ;K%hTpi!GE;INmXVX6sDg`3O^JdCm;B?`aAyt1W>4+6IhEYysvATf$KfIjndthfVD) zp}L&~WbmGe&gSsU!wlRU&ER8=8T6Rh6xM`Tz{bGl;MJ}rBqqq=%-&XT{Agpo@Hb+uux)vsQ2OSekUaIM@N3{P;lcJJLhPD@!j`xB!m|{uu=!n<5bKmFEUeoj z+|Jl7c)9Ns#$;>}+Fo2QnAIE-dfhxKEM9s{u()|dIM({0uqY)@sQ)%gXsFyH2sd{M zoloo#tXAw0F0I}nj4$6VeCV=O*!kZkVaLn@!S-r_pu2QJNJ=~+e7>_!Fx!zW{A|2i z=$E@quxhFxYg9VEbgVu*LVZ(CQEg~c`3S*aB=3w8+?^3#Rbymi6?zg0s1;AG*wYmyL|oFrV?lPrkJ6u~PXRXAh0 zR*)Ix3ynMG3R9f3ga?|P!utv7!p-8fLiMv`;l{dU!sLZ}od);~h2#3vQ$eMe`GdDJ|oK$wkq^-}G66`MgM>>-Cw! zCiz^UL)bi_CSjpa&^BHe@OY_kt2{wyqhBV>d7B`FHj5LUub{lS$29nMq>{EhXQg z7E-GOODVIfi4@S_Phk_UmxZ6RiwcWI78H*3mkM1Y*Az~rRGw6%^Puwk~u^@h=Q>jVYWyC84leZcO3f;Gn|D56*@6|5y~RKKG}< zEoi=b`r0J-wPuO#7kbQaU+LV({qu`kZlCXEyZ)CmzTol3Ck2Bun-vyDw<}yd*0S*Y zzHbG?^3NA!Z^|?xz2Mq`^99X%XbZ6W%7T|YKD$>8G`P3={@LAY!87+^^Ut{7+q=>I?8)Ko ztrK6koeMB^yL0aH$+Uo_1x?Ei6llCJ6oi;uE*N*@R6+H!=kAHSD%}IkU%1z=(76YG zKjL2Mx7yw8&Jg!*;cwg;zO;7h<4Pxs8m1ITjjk57{#;!!Vqk;9)f$sRv)5bQ?auFZ zFLB-Jp7VEuds`vSz1{(D_f3PFx?A7d?Dpi}FPDPG-UW7zE*D&z+pw^&i*=#gTv@38 zcGb=6=@Yk7;gwtcIZxagF1_s5-YLUvTS`wi+k!yn$-nv*2>s6&9O!LYc+&aRRtj} zS{B~h;#xSaUqE5lwE2a*^eYQ1-7*SamRL~vFiR3O7WAxfGivwRoD@6F$-Q|~TASXK za_2N9*I#B7y~mh##5AM@FMdgAsFo)7)l0X2)T5Ej|4J?<2C4AYS7~{RFH-iPuaZgi z4=H8%Z|UXSf0FsHf70l^za@YCCVdEcCpAoYDuw&rk=AT@B_)SHkngK7RAMajC^}H*kpk{jMPuOGWaKpQj$KxU?-%KYRFuN!@=N3w- zTaQYk=&&@W(J^U!Org~1^*QOKeUY@yTqh-Dmq>Tl-jTF>Z%gh!Zb;AaE=v`@XQUxZ zj!32PTIq79J<_hbJEgO0cT3NMv!%m{`=xi2j!T(aPD!6*E=Y-MuSlkciljB?iljKR ztCBO;QLM(o$W9RF=A3Dn7JVGV7KvjVL)H?R`}!EipbPz4&uU z^1V|eIYr)(mJB{76|LVVH93+c<@DPnB_7=-nSR_VE&RD%+M(Jb&B@bBS+5RC4@aJq zycV930^Kf48{=+B*L+GOW$7`=Z1Q0#^W=UhyEa$4b3R)#YMdqIb;**pN9IVC|MI2S z)<-1cdWF)Z!RMrrIaj5Tql+bLn|qSp{FpRn;c+Qw&vEI>tYcEu=p&Nve}|-ce-22$ zGzTS%s>9OUJ|`sG>m*e~U67o2U6cAAFOhmIe<;m9e^9bhAD0d{EtInJB*|xvBn>wy zlnSSvlS62MnR&PeW+Z*P#@)a~QSnI*j%;975V2p7iGHfAsE4Fx5VrNEVX< z=)%#F)Ng_p4e=dJclZZ9<+=UIWl?|9zUfcb9S2aC!u}*S@}P^ABHf5}ry0r9=+@+5 z3TZH&A})*|BkpbA){_?f>r2n}igYiTe~j1xDcS_-=|P|^B?685=Ze&ool6i#-_1*14hRH6ZF?OLIgPo~+v+2~n zVK9x097k4LeCeQiFqt0eM`N}FT^HQQx|ItJEp?(Py_{&~zdm$iloJ&-bf$Lyoaub4 z5Hc>GK*fHe$##twJ?K7=4)ec#T-{Zmde6B3Jts0c*oQ(o^`R)oJ`}`1I2#%1Ope*E zl$7aD8C^$`{bnzEvvUyj&+SK)1T=l2EB)0v(X*y~sIj+_G6W^5vXwOGZXfDW?o8Lt zxzV#PgD855CsivwNa-U|BTskoT6B;N&2 zRD8yjzTI~w+a}HwlFoWk9qD)1Uew`%1Ks^%M-``SsbPRENxN<7&U!oY{9;e7&h((Z z*qb_U=tI+QccrOsyOPVSu9TM0g^q|eboELFok+_TuaUCr&6!R$$$e zG_NF_J_k;xt=VBTdq5a{G!COv>!y+A_NkOtyM{g;Sw$}|BvFHuWz=Z-5(+PxN4kjF zG;c=){fP~ybGyUn_U>tPsq-{4dNP&vr%$C@ZKl$+39D#`MKX<=u$=BMilasYVraY7 zT$=JTf`a-oU%jc+)+UsW4-26VCLuIb!8J65N<%}a)$bKFsUU$aZ)B}m^QmLWY`PYz zCgYN+WEB!j0mCMe&yqkw-2}Q&J%MD40?E!Ph_>Acq61IkX!f{;^x~6-cBIcFyQSeY z@^UDhIy#YZc8#adQ)5YQ?oaV6#*q5e7z(`RPc{`}sgqd%r3A#1{Ui-}&7MikGQ(+H zgDI3%JdwKe9Z&67_|t-$qo}XvNb)#8f==}sNt*vgkv~UsKN?c6a8!sl^ z_jt0cpFoSW@nlyTN1BOCDcfiW|^Xc~E zc@%jrnl4%;)Ahha+L^hWo_|cBKQ8gK#%3u+wpc_frpHi)_k5~Wh$>)4zQy=^hgSdiJ;H_&7#-$XOmTb4VjuQpxW25^a~czXmJsR&Rj?&w}=d;Jc9-C)PKn`GHSen(zef|ElXlZn7f#=w#HG)`*_+Gl|UiW5-8X-fmYv( zC%s7m?RlC&1w)t9e9J_#`;bIy`mUs1UQ6l2ta$PdTSm66R!~OgM0&e8k;V^8q$B)G z85@dLP@!2OKhGr6o|j4dJin3-r>4-rX{%^}dO2NkO(fmtBns#2Wi?#?R+7_>l@!o+ zCCzx0Ov2A(Iy8AD=}b~ceJ_Oy=B}cZxvS{CY9-x#pF+JLm3FmUO{re1>1qAd^ks4? z_03#G&#YI`(l;q|;du&u{Fg$i?yG2q#VV@Un?jG~uc2olYiX>*Itou&N1KAy(cnvK zY1iX5)aB`Fno^fa!#q>z?HT47v5K;8rBJKpDRjVOB`G{MQt+aUwE56RGTpq9Dy=ut zn1BsbF=#z?Zn};FZm%Kl{M9sUXDa1vTSc0L6uK!g?otws8I?{ZztZUDwlo^rJ&oF~ z+)P(!6M=Rk8I9RMUix+PHgqkSzg|s#5vgSHDuo&fE6JcvB5Jjj-VWVD_tG+`*S~a% z3{R(kQ)!fZWi#Dhvx!XFZKTY3>&fxzS{h=shSoZ!(%6A1G@@@ZO|;%dnPyu_Gi(bz zxSm1jGcxEo_ecv(rwN)gvhTT>PVC%Bf2-Eh+cxWHi_;pi$5i^$DTVy~r_ivdQ2Nny zDwPbMPUer)w8uS?mhjhI=&K0QriYXBys0#4RWSde$wW#D3!+sm1B)M*y&NRFgg8Z|Ai3?sXvQ|QE^5So`6On=6P(EdqNC}vFv z*%pP;zxrYH;-8A_QX@$?K8v>gm_-BpXVTj22)cD8oP@4n3pNc!zDiw*_NA;YmKo>4T7^PWdbl#7Al5lc4A5Fbp&7R{63?V+KCKWI@cXz0Gzns8Hl|jJj;ocKUYk9}Lpp zUv*M?(s!w{?H8%(u1`|fgAbCl+lXEYf28_5en{Q-evvl4tCiX;`YgS<{8h@1{2@(R zYLHG?|CF{H86=;5-zB|@_ZZhm+vBPwx5s}Zo9}hf!8c!}s4<_V(zCTvZpCM5vFe*N zqF$YJuGAp8hyIivg&L&hjekgO8-11fe65kLum32G@AE^lFZv?&?^i3?+SW(|i)*BV zy3f+JTi+zb(mLtJz@O69il35Ei=Waa?)87{eFu0|<l>j0{nu=0H+WR6U zz#xK36GcH0QL3UONJ%>l8!GmO1$zSvDsW({*b9(=f(09j4FRPo2mKGW{O{g*zj_NV-RTYgZM*j+@Ald+KX{M7 zwb5HLb)$FC7e9D)Pu=W&J$jq>;Nd&GpT_U<8t>Tc^}b@Sw`Rd!Z{ufsyt&=@+MDk3 zo=p42+f-|V_xauHy>oZ2_h!u9=$&`UCU4+nTfKvtZ1d%aUX_|2=w z=Zw$yda>Snye@-&^>)->??pPU_lkF{_nyCUqj&K9AH6$fZ1Hw&_{qx~_KSDuUB7xa zcG>IQ^2=}D*r$H?T8{kP`{knFyf0tf<9)q$y?6J74c`2w8@;RS58j<`|LA4^y4m|_ z&NlD;ncKZDp5N)E*xIDtv+urzvdggYES*()wDl)(-J>= z?wgQ)O?!DV4oePgL?AnOV_mdQH05{N*(7&S7a@#+hl}{HM~q_0<^bpYCnA zAl;iEOZBF4KI*E9-u9gpz4c#I^eXnL=v_52-MeXKn%8|@s<-f#RPU9$QoXq!qE4j9(!58SrFqYdOZ9$OTfys_sNl_9 zTEWXWIMo|CKGi$xzErOrpYMK3^+xST^M;(9?iKMq17@UoXBMS;U!R!joqJ~m@3%%3 zyatUccvFT~@aoR3;6+)V*_r#tr+U|KPW5j8HO)J?Q@S_)+ca<0kTh@3wW;2mgHpYd zZ>r#}?Nfn$D|oG@Rq*P6SHU~AO{&){JJs8CVXAl89OkW0^X}`B?p)KqnpX9y z%&g)KtWw1rx8jdQX%ANLZq7>gRz6+HyJXWr-h=N|<=60FZ^5nAy_?Rj;axPVhIdI$ z4e#~M)x8%xSNB?vKiKo0sOEjOt6GOe)fTmXX3^C4-wy2CzWM7@+Shz`Ui(QED|Bdd zV%rX(3&wQ#b=D0X`p=!+p@H{Whk{#HcX(>ih7R3--__xkZx^>8vba|J&~snSduHU8 z`SYuOH@{xBZS%g#`>0)$I=8kz>-}HbztgsDhg%z7++p--H+J}L*b^O^w|}L>p1JRL zC^+?^_PclfGH=0~u>}oZerG|-jm)pc6x7JTg zXtHs^(3fvoP`&%>^ABD3X}hD(dbs`g$0~Lhcw46qQ@R~Hzt*m27BrZ0WMbynvlFl0 z9!uQb_u9n3@2*P>{NRd2rNb^s@-- zZ<>;LqeZVoqQ|sEx9?^phIV}>@yD?Fi9QAM63d4?mH27e{fVFZ-k4|-%}YFfM*qag zs~RP`{_@p=V{U$H!M0PIC7!N1E^+qo+YSk>dP#JulsO{55q+bb@- z|Mc{Qsm)RsPP!(v@LaExeSCH&+jLeZTlbDm_Qj_=+3ozFHrUyxv;Fdm&i0fCyV#aD zcD0XQnPoS|y4jk8yW1LjyW1ug^|0SQccN|l&WZN!*H5&K_^erRqW$C76YZ29oouTY zJJ~TKI@|OXU2Nw{UG1C=UHLyFpJnHs*Ub)G-ObLe($U`DxudW*q|LC#%tP|})XLhnLRqJdY{;;zxzNL$Or%hM8X+c*z|J4q5(B2Mq@gE)R z9o3DkTF2NM+Z)?qw6PB^Fm`hXYv1|I+HJ9pw)OER+Ee&f#&WLiWDm;dY)@X^)_zpE zoxLWbogLb$on6?joqfM|JNtF4ovrXvJ6oqsd)~dhopx3S`w7pe^^~!Ny{t`NYc2mj zB3m!7we9~{Yy0%4t?e1Fw6>qV*V=wp}oCTj-4&+YlpS87k6xFpC8lGroYfJ#vXQ2Gy7xnW9^w6Gi{rNN80Lx8``Uv zHniL7H?l3qG_sQ(Z)C6gqLHoLxUoGor?IW^abtULpC)$TXH9I~X-zHvKh@f+`1$y7 zN)NkhW)C~*(;jwhYqA^fCHrRWp7xEqd)liSon+VFf0BLYh+Z~+WiR`}*S+iwtxvW! zE06$e#A{uX@@+XP#siy?Tf+Rc7ht-Jj^+}&>2)ZMN+ zr-#jcrH6g04%xmJl0AGD*=yLG_ zrfPRPwqG~< z>b2eMDKB@mU#Dl;yg^y^xqGtgmm9L|gjU_`!`a>Jh}qq2TaWMIX}s5^o4eXJe2w4n z>pJxs_NkO<+wN^(EB)NS=5B0Y&-tc-eP?Y0+yAQucII~t?B*XD*rII>>>b-1*t2#t zu*F*&*n)KpZ0!#k*d8xt*b&cV*ilbs*xV;F?AecI*hPVGDO;*dKo2oX-~b31X zht;+Z)U9o2A5q&@s$bh4S*Nz`Rin1u5vpw)-Ii&8x+&9Mdv&Ir!>2pfm1btz`|ioK z-I#xOquTa@V`|$UkE?Avw5V;X9$VW!%h=qE+IGU7nYQ}vnRdpEOq<1LH*4zgx#7V~ zyXq;vKl3u}@E0=eZLeh7J73MT=P%B*k0mngw{tRW@zo9O4%VD^4bQ$l(_VH#1sXz$F=w6E}~8qMVIJDGOK zjhS{O&%Ww0p7UI$H9WTh&#d}JrhSV~CTpKs#PiS3w0H6O_R1hK*ioW}mv$_^Yp<> z&Fv{mP0{uxrt-QarvIuX=BZbgn4V8AF?^tSjUcJn}C=p`~W&+sn-8`<9uNS1dC_ zE?s7dhAcBj^;%|bw98EMX3I?5I?K%Pip$KNZA;BhA1^gcUt4NgJhs$)!TVl(VUelU zxyYQ+sK`vLRAlCDU1m=In0?+|W+uM0%)I*upIdpi$TBl^)G~AEY0J#DEti?>YRgQ{ zZ$)OzXGP}bmx@g72Z~Jb^deJre39ujqR6~;T9IjDicHTYMdrq;MJ9FoGIQ5z_VSjQ zW_K<#BkwLYM@}g=nWq(-)@_PSg`fR=50mh0rq_Sog(8sS7a96Rb+<6*=tmh z`SzqD6XiVzw^?rbRaeqC((zs>p=icQ*G#b!&i*t~d2u^Do9vAMWMv1!q~*gSSf zv6=sKky-S9k=gW2kva3=PfW|Zmz&owTW;Pwd%2m}dAWJ5;c_$T;N|Av-Nk0;cg3d1 zyT#`Ag~ev;J;mmWaIv{=Sg~1ei%o-BpO~EuR+#kN%gxx&mYcI)S#IjhS#H|ivD};< zU2bwOUT#i3bGeztp1F;dn`x=b&4RCr&A7$ICVj~YbJ4vk%#_>}=Kix+m>ykLm}*C@ zFk5P@Fb(!DH#HW1tt%&(d-yDQZ-Tk5c!GK5%?aj+mnWDz=T9*A&6!|U-#5Y3X_0Mq9-D3EAD?Z$ zYn5$|;`0b&Ex1nS{EX(==Dee`P1=#!=J#6JW>}?cQ*GA-bL)l)rfoFaEXvI`r*KT0 zmTd-Ik!@=7dGG3M6TOOY&QFYIo0(IyO=4`ex%b>`^Y&>RZMOM(X|`$jcD5N=lx_Ab z&o;NLWZrw(=8Z41%|mOlP5Y0s&GZkm&5y;|X5M1nac;I*IV;#`X+jGoW z*XNkwSLT>eGjhzzhjYv$_vM)O59OFw<}mhjj;Z%dj_JmE2KR2fHOJf@&oL49?O2p! zw!Oh~U*>(@&M_Y>%`qnx=a`+Vb4=FS9P{C~Ip&~sIp)=Ga?Hn{=9s}Na?BX!R-Dbf zy*Z}&wj6WF<{UGhTxn(Bv5G;0`}#=UE{=9rnE z=a?SNCz}456U`4tOf=&gOf<(fnP`R`H_`NNGtsm=aiUq>b)q@9+eDMnd7|0Yexhm0 zz2WuPyYfUcwr9wU8xt}YPv`Sc$Sg>N%n@&g%x~|8%;XP3CiBCP$ygOK&5J^&?wcX= z_=_R)*}RZ>_^FWj{LGLUGBsqD+{L;DA#*Nsx_=fj&wd{==WSx$mXKM`@!1U_Gvk|( z$>Uxwd(BuDGL=V#%wd;@%=wRn%q;fpxF%$ZnfuwEkohV##avW5#oTjHin*_1it$1z zX2tH13IEI<>~-pdkh$)Lka_*NkQw-1$b7)q8{8jVGsX0-mtr1jkYd_2PBC@)oXq(- zbyG}b?rmnTmQ#4onIZGz3n8=SMJyw8y-rdkW;wo5S!IZ*Ja0sw6tn596x08L6w`hT?>a8UTsktvd~{xl zxngjNdGtztjrcyj{TAORzScu4rI-sFrI<17+y9IdbIPa`^K?#%c_cT*nEVuTHpeQH zQcRsoQp~~E@cn%>#P5+IQ^c>!T-FY4mSVQ_}GB6gHp^$)^23&&%9e>o^i?*DQ46SDQ3xSDdxA^Q_NV7 z$KTBNto%>;9r!ogf&RUFo?hYLx)oB|iFt+8l$4wQF{R2sVvT>qAUw5JC?ym+q~VdF zw0+Emp*odAb)uZ7SCLNYar^OCqm>U8LTbv0#`}?v2XUMKWYpg$O-&E|5%8y4AK|5j zYK5xt2@UTzYSM)xN4KL16DE!vcj<^pqb>{8tQ`7tfeI-pDXF61KZugcxC(r#b(1_b zRF%!duGhHn;|GrzKY5hzWr*AUMfW?f0snb5TCKCVrSeLZW%7k;nZuZH1RohTlGmFw z?vhar$B&paZqnqDoJ_cM%)YGJc-b#CR3lVV_UPHC?@9dz_37Vl_-TCxoe?^m#WnfV z;lmdf^1CY8>#~FRfB4VUd~`kCsjuO>T64KesiEq;HSdfY*>l__<3|4V7BX4>3~BdO zZhz{1Dqg!WYbx%;pM6}eZNFiqvKSsQ=EmoG_3qOn^lAD&gZ=;0qkpx1Nsm+aNB24S zz1^0!pMKtJQ170h&_n;w{m<8Aertg1s`*rVoQb=A_xokOkd>Zc^_Dm?$D($U$q4O^~Dd?ndPY(Kj z48Q0kIjAMNwZ@=D^y>lB9Kmn|cY8q?G;Wj~T@CgY9J%Op?)tcWy_fcJJ>1&$)jpWv zVy;GN%+&sVIlK_zg&GmKj_4su4>Kgnksc0u+QZGk1r8t768(IDVHZTrcQIXSf5F{e z5T8cz~O^hqMr{i?1HHIE~abkFSy$a!n983vX86LJ0jQnX}!)hSMMb` zdWg`uHE=}jBbe}A3|t*CQ~Uen@Ir(KYDC~VqK7Cw%#bKYdN}B54>t!FIDAk`^z#9R zT@W?j#dNLx1$TQvnAYiB_Hi|ON91}xt=GBc>b)dK4-q=I29BtG1QWiCfvY2CYJa~R zUWo8OjR;&v^bn(`n69k5YKCVXZ zh+OZd^*YyFy_e+ZAwuWYz!9~NV8VAXaCO8??eCYv3lScu5rON79-{OxL!un%;h?8I z+#Fos@Ifun&j%QGLDYN~)3x>&-0cNnTBmc_$JOW^k?Z}mUgw&t_mUhvMCjZaIHL9u zO!zJau8x?g{rz%yA;JSSB5)niLzEt7NR%Tz9Q3q@n}Z7+KBy)7`2fQ%h??(Wy4L=J zyS*Sx>vS&rxEj49a=o9{>s)j7UXr7S2%TF4N7O!o3E#!Q)e$qbzh4e7M0lV^1g;}` zh|u2Ch}uUm;ky{PI%1~w_sijh2oKbVz;#3qQF@pmQI7O*(9<4n4lZ!`pqA+8 z0}Q($YQBr(N6ggz zemT4l;ei?vxQ^%{N)Iz6%8?!pdfLOy!37Q<)Dr!CfMFLz&37?fYk$GrUJ#~rI)@(7 z-2?k-TFEfa)&SEH8es5|TF*cq5k2kimxCt}x%So=yb;mUzUWEBuJ8`x zX$`o*L<3#2c`gTB_(;^4@OAC-^jv6NzFWfsyP!q{E;{7WlU#dAt~r|P@?3A!B}Mok zx;q5v!4K*KFD*~_`ud>v!=xv^IM9}6mexRrS%`i!f%(k!&^u31VAxaLX@56uOJzU~EKuJ3`=GFQ)%8prk1xa87zwXPs@S)NEz%a}8(OmSjMlfCL9l_U8xSH?Qt{=FF z@X|GQ)EIclB6RLdtpTSr-pRP=A!3G~ugejxV8C?_ABp0nIp_gH{dY#0iEBV~{r%=^ zEGXB0!qGKmB-6Tlw}uCzI|JUX4mJ8(FS*9S)tqE~;kg>rqSIPDi1 zPt*qz;fX!r0UWhN!89%!U>esNKOHbcc<3D@*R}S0u{5 z+s)wtz1uIT#tis?4;-~b!Gu?u-L#*dm+K3E;exL{G+*Q3>b}nPuF1IYM1%)465*ve zZVtZ2;T062DXH)0>4&*K+F$s(23IhRBQMEcc*!{M!5oQxnBdw+@2PR^&h`z>3@&dOct?t89F!vfXPrT6w zj))%S>!>*zcXM|S=?exNeC&W4Jk%~9xQ==!$+ZT#=7<(Nje`qs-Gj!(1cy%dfN3um zLl54``hGpkLr`NGq)s*){A3=b*#k4g57z^o+rvyZYM%6Tjr|a{78vH~p7szNT=)YMp4RJJ`-vW0 zaNr4C^W7X=iO8{o=E55oxPB2_MA0Be^uyqVo?ByfvM3ro16;x2=jNCvIJAh+xDkEP zqNiuU6Br_Nh`_}ICs_}C;ot!Ci||5}8ORYur*qdwdg$wG1&$tcdIm5zY9Hx|ht{DF z+||3a@S#Budm#$$^4uC)9YwD_;G?7F!&7S{2kvrpt#el|J@L>He8DvzI3jdztu;Cq z9`<#$;JJO^Zp2K%(bJggkDeP{p6ivYuY01C+TF+X)AO|+crrdXI%+;<`(fA(TFeF) z(d7wWdV)g(Orpl%?HB!cz%Uy+w+1eJ>A8LQxIEMn!Ev?VppOXL)w+7O7QSHO)3bCgI&k3U_JLyt{80;EdjLO>h}k+KFO46#67^2lS#WTGi7&Y5 zC7YFugC6`a12}x82BvfLCF;J$#S{ME=^7X`@Imd@cR83LQTOy5!QH-iLyLLB(;mQs zqMiXi&DDLK3nyq6=7WzKzPgs&<)M!Vog2}Ihxlj=J3=Qta0SC$^fe|uzdpF|(i)vZ zkA0xi5gcIdJgJlE(L;m}qQ-Tu`+~bYsr4>yU+a+vML&PxYJaJNW(4`VdUyhth~xU} zo%CF2T|Rbjxtb4+aP(X^hnE|Z@$_6*huYmm*U$r#8kpAW9RzoIt{ycayd(dg*Bo!F3HSFo|-4^e!L%E*~C< zy6@(~@$=O@%#o;Z$&=MCANydQ*1DMX_Tzz{EC%TW*VmoQLwdr+0T+=(|ySYK>^+fDfJ<^$hg12Du+kV|q4x{385xPh)y#jcb0gT<@Xx z6OHt6P-`#rv<`lVnlHJ_1txsW)jY}3N7Q|IXuXTMe82tR?dRk6+}fR|`^XW6t8>2^ zI|xtj4jrQOa9kg)(|*WH8~uEsm7eUP_dySs)YuW2_5>EBgC2cE=n!2TGxaQR(32Xu zMAyqtD;(f1AO3C>9**=}9sCeoJv6RPd%7I)(tMr!&5^#wH6Qzw)Q5-WL5uw)>OM4@ z=jPZ&>s*emg$Iqs1;f#JX>xc6O@Y=@JsE>Y)t9&nA7x|B zdm-06V9BDM;qqKBw-!Ejfsb&IBMJts?u#C{J6mgmc*1e@g1I=n5HU-%TCa2AfR6~B z?g0zZ2K9lLCW^PagIkLaFhs$i)e$oV!_j*5foVVFh~Q`qF!4sN`x@8tH3vER&>-qQ zc0oi>qKo;}8WVqA!%HwZZeMEbq;=qm9({PZQR{+wejdO?@8^NOMC_w^z}%V8xwv1g z{iH87Fhs${6MK~8gZZM@IsD+Gdy>02=3*ZBsNERU_rrlM2QT=;N7vvWq9+kM>Kr^BrH>;x_!2cw za(E)TeZN}!Xr0EP(|uq%O5abXImqFKC_1eLuK9xDXf8Nz)E?mb%|TD}I@fyiFdwx$ z)2+3i*1$*ldXCi4XdJ#8LkMCdUGGXw`FT#ZAkG5AOieeHq1ME8gfyr4yNb%F~Q5nkYHFZ5gtT=WrvV;A5O zfk|{{>%N=A6A_+LyBK!o!%7anTurS+~a_`*R? zFzp47M9tAWJx}MZFFZ6ZIp%4+Bp-J^yd{?QU@o+nsc}Cq;Ry#kiGm3i7<9l3ed2S7j_7@I#Iu|Z@ZUj$o z^stAnG0*i!jlO7HOwV_{F&`Stlv;bC77QFj?Wr}`3G>0nEa==?crF)pvWT9Z2XEce zd$~DyqJ!4uxV6?uPjtW};y|apkz*F3XpoCedq4vmzKFs_uA}SW@}$<@(E9PTm*%-# z>1nR+>+6#oJmm1z5ghHSch>%r>lxZd=g=U!8jb7R<>^{-;exN{VRy~feP{)9Iq=lJ zz_bT)&C{6n)f}CJkEk^+COsUt2T$l-9{7mDmjga&N%Qz{lKV(Ql6C z!yi%iM630}(KT|dm0Ta;pw`~dz(e;W7u@Zkh88?%(U&M3iO9i)7F^VBUuxaM4DAaK zH$sO9Z{dLpKSbgB@u39|5k5MKUTWxH>m3mzi0E?4*UJjpQ!9B9EqjXwC8rFkWBTrK#nmtS4d z9Q1L2u(#`l9T34oEqvsN@b-(~h>yM=%#}W7Ai@V)sU_DnIPgX77lU-b^<16nnV16% zT0~bTdkCicItL%o<%5f;uS+n|X@BVhm+0zH>v`H!=h_E&kSDl$7d-=W5z#|*aqZ`K zP2i&^HI87|13b*Zb)b)0c;L7Z9Eq3#4lr=g2Oh)&A5nOk>*iA9S`aYa__#)Q|B|%j{l7kM<@clfew050<*KLh2@z`x@eDF3%w*`BMi{=4{JdCii&HpzPK%(8G@?f%pTe z^R!=(E(pgwHPxEOk604_U+L@nBR*~&>=T}-R+1zCQTBms$FYOL4??63*bb3E`7V6S) zz)cqYe1IWpy&r~t(DjtmcRj_YBrkaSMc2=dv$|P!FCiN8`9g z*8{agSJ2LSCqJKLeLcg~N>ASpcfQ|F$^67WsIR@i!8}C2 z{eWRtsa-z&z?BF-db$SY`e?n*!Pl6d7Be(Ya^Zq6(e*>mPv>%74*Ef}G2him4UOK< zkLQPBXGH8FTwu_lhX`Dv#{Bkhy|mWV>z=D~Yt5A%a5V1b@NlEcL#?k(Ft>(}=4+1I z*R|%ACik0;I(UGO=;F|zj|gAXI>IA}qjAZ>mxx^aP=l{~!1P?? zx~Dbh>!|z45y6vMj-Rh!n(x-Sr*rLvIc14KdOf2oelA!1^|gqvo~d(agJ#1EJnah% z5gzbI)O{C|p2m>75gvl02TaGHp6rJGG+$$))wR~RxOiv`bAcgZ7W$|UBw|0;8#QJ~ z?T29|cv9=!uMbQjJhi{>ySbk?JhUHlZqHAzxzd*-IzKM<)V`vJ4mmvG1wK4cgNKN| zUk#1Rak=0Dm#8t(2YCordYZ3&kVEhCfV&YoL{}#`G~nncJ+0AP!F3NhjcLByM-P0| z5`k%rVBlz6W8i2HV4^`TQ4V~x2QVGMMU-BU#t#Q35ji|Grh9r8cF|Z#T+s+0Jgr4f z_rb#~aC9vk?SmZA&qHJ2A!={Q!A11j!PU66XvJTS(>X@ z?THRO_~_c@>z*Iq?Mp2lIOwCs-V(73Fg+VRJu_J!y`YFa5QQfPbD%>Fo<#J3xixS< zEigBV#t+jteD$8t=p6(D?nZc&C5pH9M-A_?c!8_^fFb(z{4ntSB6h>B!qK_Z;GlWYRt{uxw@}CF;`>4#a>zmPxN(9 z=kNpuZ{Ud1M-REKp-0s71uLnB4iVnaNsU}MQoER*1HHzy9=IPCYxI200axz{KVa}hEq(0=58%26hM9=qXdkx+jzsB!D>-WT zBcdl!>-6q|qvw~qnD#;sQTs}c8XDmtM;{z`=sx&@X&$uD9B9NW(Ze4YqV5R?IAYLj z^!2Wofhbzxf$Ms?c(U5hNAq>=&ewA_Pjbyg4jeTiw5TNllNrGM=7EO@4r+;V;E4zx zyfq&^iQuEJdy>N&IW#U0xaMmP_RttKF6P#vg&tAnU}ueKPv{V}FSHudS{IiZ*M@n( z{J0v^e2qizw~N+DAG2LOJS6&EXAtH$6J9|*zu9_E-N(+r^#0Ibw;-;&2l$A7eO!Y? z@PJ8;eRPh#t3eG7YJDx(-Sv|iJaC|gpVsIceXWyRa75Qj*U32WMs&H@!;PBfr$rx8 z@8idZzvc^1*T{v79A4UAdhi1-wTo%3XyAzmK59hhbS+$63&*b(OyhpJ#`OM}>&|j( zKMm#zR}Q@VqUOR2(cKMtc%qM5`oMIKzC@j44t)J;-9s)j-~&uY>FGUm4|tGXYlREG z&c)A9?_#Lo2@l}#h97G51s4o8`rzxn?g{Q5w=bS@g8ZS!Y^imQKBDeP9#qRb%+$H~ zNR23ZKOQug8x-|?&C$NV{kYgg_w;P^#Y5*(iv~w}@IcgaU9IjRm#BTDcD>v_v>L}w z@I%zN>#Kc`Lx-rZLF@f;7lT$uaDbu4{2&}W^mWuZe6YW(1Mc#G`|*KE#4N2xEl|C@P2%hc%L)0EY7;uT2FTUtWj-Fo>3^;tW78rI#PwRBg^>TOd ztD%>u^}uDOo(mpuMDXDOolx4h0}m0i-M+@%95V&iKFM-- z4|oDc1TJ3iLqty^_~=QET%yjw2S<83*S_Ea!yMES;jOtE(;VR9gPF(?;RCMJIKYG( z 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 XWClR+sZw5OD;OA9lSJK^< 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

r6QAuQl#;yQx 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 qt^^i{Qbzf4`EniRTCnD`?k)^qfmg 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

|YVP;#dk>Ft z;UcXW=3T$O_0R-edbsU?4od!?9oZc;o|8G)5oNUCkqtyQ^iFk9!O6N^`|<{QjOnPw zH*}KhLBLTsKRahH-$sS?E}Z^?I-N~7Q-}Aqdwka}RG&`7P71D1ti&Bg%zHAto+65_ zA9WQcM+f3U;w{jhWZQyB@5esB!NLet?#od6fd=k*xnBp~mY$?%Hp#yf!?zy}yQ)o4 z6gqeB2jyq9J=h?L8J_aHy07$TQz7->JSxg_aVZY|_}}A8K4?N)sr6x0?7Nnx0_IO& zNa%1uzsCmOB%RMv(A>{KwY)@mB;nCKFn;8X&L>Hlkins75?DEp@>+Qh#iQ;Ly)zBs zbHwcvif*#-Dith-U!ruuKYy#idFsrCpRJ1fH*(-E>2bk5pN(GNoFvWof)@^}e{0Jz z^tg9nSSqR;bGMQa-f-EKSAOX8`_1_IW^=^LEqMoFcnL14udkwYGJ=z2m^Y_A{7MbI z$D)3C67M7U&*!T>IPY3GZj}kg$NJYvMfBg1VZsVcA!9nyh*qZN6^p>LDeF;)M;%Kp zq)@=Itl#prLhmW>IQtr%Z!c$>L;p~k<%q!bj&$Z+f)lDYzF!H>!APNs8Jvq>xu<91 z>~uI`F&y*f8KsnH(F6QZ;V017_oeTDLm$=^&#A(TvpR1>`kZs_oDUbo{3HKq9KMg% zHzeOJLa85lKAuK*iM{3&fP3t%$s<=d88=UF{6uBvb;Bv)wV%yweTphcG46VUCO+QG zszPl(?2Xlh*Q@6h_8s-peaCkJ9{1aWO||Ie#jCsdFh7)AA;<)^yZ0$G73Dg0TcICa z50tOi1vl95JB>XCFogaOX^WyIngi9aJyaI)ii`>2~p9= z*<6uXfZ>q*H?-okAyiXt*t#>PQ3W4i#iS@V! z+zJ`HC1!XDWoh9psN)bFjXb=RL)!?onwus#8AG1lLC2n{st2yBNd8WTrp1viT9 zqtidMmCpUZ1HoD1)RgXdces+$CLbekzq7WQd#d4V=H=J5pjQ&pdcWZDqrXeL`|v)> zHU{?nf={Dh|5_8Si;4H$LvY`8(ozb;tJk#s!~;*M(^0qyojdSkyCpnk{qGCCXu_2{ zUHg{=wvV$^*O3_~`#%v{U{%Mb&HG-Q^ zCjF@wXTiJLYM0>h+q`Ex4CnQ^jWKPw>qGbb=TMFM_dAZkeXDWBSp#m$VcQF4aNU%i zw;01^p7LKi1dlmS+NS_LAGDuN1zw+f$pq>1JQq>Lk}WVxNF6uj35J(96M1V1J@~lc zvJ%|u7pH`SQO-HBA9<)YeE~s%_c0U_y(I_tC0X6DKHSfPM=zMdb${mj?F?LD@4aUy z(95y>OW|;P+smYd;T0|v1e5l4nEb=U3ir}Yy}4<$W5GIi9xgvMwOB3o4{!)iKL;-> zM66a1dkTk?KQ)CL^Q3xI9Ip7sw?!x5#*Z|59EU4lx%Pn_9^dG9^*Yp}d5`>UxYb#g z$7axCr;gvx;69mGil5pyOL)1D&dk9*Hs|R<3D5n$-u_N>-OBe%Hr#9@gK8_dlj}e4 zGr|3`qkD!2?rT5U2yVD{8vUR1!rfI-Z90s;5;DunglqNl?K#r@UUuc1`hw1w4yXme zRgw3Rj#-~3^eO+cUEi1{zMn7{rrw$+Qf_>{xd$D+KVNng{r30mCq1$mf-7INVgnsj z@zXp+K11lOL^ejEr9^1Pe-tx>^wo!bkI~SD)|^|EGsK~bgE7ohGsN+GOFxs*yRLUu zPHdYYw5Wn)Ð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

_O2P~P3nXHDyo_fdAfQx26=_N7H{3 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≦5$ao=hB#??RaxK2d0bB1c+jO;B2#Tj&&Y!Gn2>jJCw}Zf6LG& zk1#ajUhjmivYGM9ck+m$>QDNk&{v^#QPnn&xDgrD9tlPHKQFey%Ud(AmY@hnf+USy z9%1Fp$#nxVAK7SZg{m96#<%SA2wCGNs+;h85nJUE%oqGGY`Zp`7fbA6!~8e(+8KtblHqoPz3)o4Aj@L~4r%A{(_%J;fbEDakOG)!3Gb9%# zp*QVd4JYQ3rRggihw4__Qtjc}$wIv@XtD9BNfezggPPWn@X-0<{uAiLm(eV=!|PR+ zxF_gZL+V{Yq}7^_{_fAANhvnO=lTsaT{wdbAAAB7pu$D;}$qY1fU z5lo?HkRqM$PGHsgNpyOL1(nO7c0}yRVRUK_b2g85c_ zEH$u$D@$7joz-S zZ2NrAb|>~7jM-il2NO)h*|%_C`059F7btm`CsGUkN1g3f4S&1XtZQN2?}CuGW3XgR zqT@a~X3yue7T~VIzxzbE z;d-<$bV32w-x8(b8_(qzba=yW5m+bc5$NVhU&GiOo z@7Jcp?@0Q*rw8p|NM=XIA*?Gt_)F?Lq|!H2AA*H4_s4sofzr@Tad^_)SNJOCznzFr zRe{MTlo!LH`d5Za2k<2FpNpu+r~!bU)_pqUm!^+i zc(OqJ33OOi`X>Iuh}1E@V05^e`kd%o@(9bmDSv->r&%?(bTOBhO76Z*a+~P*%TXw! zE}{DmzImP@e&ly9LFt(NCmfcu+UQU%uA>Y4>P8@P^3}4Ge#wpMl6_MRe>r zo3j(4u!`*{13GQw7Vk3PPX)mQVRZcZJc3K%i|aJ9Cg@yq)!ZP%eBLY-$|y)HHZ*+* zo%@qhm;K{rJdCce z-7AOAx+DK^Gn8bu4)a7WZ`+Yy66lONG@NUPL0d-#H8FqJ$MC^v`1HR^Bj&5Q#P3HM zyFH+Q`op?P^tdGHE)J5;8A2nJhF;dDjHCiOI?Dc6s^J2kdh-+%47+YhgHGkGV83J- zvKS;nkIvU~vpavnQ&IM#ap+JarW^0aeDdq*&adEf;p5CjSkUAwvG-pt@$%EGDkC~k z+@;qGVeFycE?dkG?BY`ygWsO3{jkQl&f51G*+HErdq?8XduAa0TMQj7uH+h0@8sqL z#ar|)ENeWvhWQIt9D-YL;<9a!9y)<*Pa5AqQ`L*4iVg7#Yb5|>&E@#iuFx;Nt&qn%wwx#NBkfV1qYmZSr z+}9>UQi)1NL?oMx zG9x8>mQAu}W{8CBvPw#sNfM$WtH}I){r>t~@AKUE_w(G(`#RUT&ec&j3-U|!r+4On zN~$JTr$HL!*K&fJWrXm<7%4gAPDYb9ego}VvWct67v|j9aUWDsq`guH2H29nr-J8} z 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

^WtsI8$+m%{rlhBU+5R|W5LJ} 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�XSfByMn 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 literal 0 HcmV?d00001 diff --git a/test/test_dem.py b/test/test_dem.py index a008c7e56..2ddf68d67 100644 --- a/test/test_dem.py +++ b/test/test_dem.py @@ -6,9 +6,9 @@ def test_download_dem_1(): - SCENARIO_1 = os.path.join(TEST_DIR, "scenario_4") + SCENARIO_1 = TEST_DIR / "scenario_4" hts, meta = download_dem( - demName=os.path.join(SCENARIO_1,'warpedDEM.dem'), + dem_path=SCENARIO_1 / 'warpedDEM.dem', overwrite=False ) assert hts.shape == (45,226) @@ -24,14 +24,14 @@ def test_download_dem_3(tmp_path): with pushd(tmp_path): fname = os.path.join(tmp_path, 'tmp_file.nc') with pytest.raises(ValueError): - download_dem(demName=fname) + download_dem(dem_path=fname) @pytest.mark.long def test_download_dem_4(tmp_path): with pushd(tmp_path): fname = os.path.join(tmp_path, 'tmp_file.nc') - z,m = download_dem(demName=fname, overwrite=True, ll_bounds=[37.9,38.,-91.8,-91.7], writeDEM=True) + z,m = download_dem(dem_path=fname, overwrite=True, ll_bounds=[37.9,38.,-91.8,-91.7], writeDEM=True) assert len(z.shape) == 2 assert 'crs' in m.keys() diff --git a/test/test_llreader.py b/test/test_llreader.py index 292cc73a8..6c9cc9801 100644 --- a/test/test_llreader.py +++ b/test/test_llreader.py @@ -122,9 +122,9 @@ def test_read_station_file(station_file): def test_bounds_from_latlon_rasters(): - latfile = os.path.join(GEOM_DIR, 'lat.rdr') - lonfile = os.path.join(GEOM_DIR, 'lon.rdr') - snwe, _, _ = bounds_from_latlon_rasters(latfile, lonfile) + lat_path = Path(GEOM_DIR) / 'lat.rdr' + lon_path = Path(GEOM_DIR) / 'lon.rdr' + snwe, _, _ = bounds_from_latlon_rasters(str(lat_path), str(lon_path)) bounds_true =[15.7637, 21.4936, -101.6384, -98.2418] assert all([np.allclose(b, t, rtol=1e-4) for b, t in zip(snwe, bounds_true)]) diff --git a/test/test_util.py b/test/test_util.py index 9d17d6532..7943aaf59 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -131,10 +131,10 @@ def test_writeArrayToRaster(tmp_path): array = np.transpose( np.array([np.arange(0, 10)]) ) * np.arange(0, 10) - filename = str(tmp_path / 'dummy.out') + path = tmp_path / 'dummy.out' - writeArrayToRaster(array, filename) - with rasterio.open(filename) as src: + writeArrayToRaster(array, path) + with rasterio.open(path) as src: band = src.read(1) noval = src.nodatavals[0] @@ -145,35 +145,35 @@ def test_writeArrayToRaster(tmp_path): def test_writeArrayToRaster_2(): test = np.random.randn(10,10,10) with pytest.raises(RuntimeError): - writeArrayToRaster(test, 'dummy_file') + writeArrayToRaster(test, Path('dummy_file')) def test_writeArrayToRaster_3(tmp_path): test = np.random.randn(10,10) test = test + test * 1j with pushd(tmp_path): - fname = os.path.join(tmp_path, 'tmp_file.tif') - writeArrayToRaster(test, fname) - tmp = rio_profile(fname) + path = tmp_path / 'tmp_file.tif' + writeArrayToRaster(test, path) + tmp = rio_profile(path) assert tmp['dtype'] == 'complex64' def test_writeArrayToRaster_4(tmp_path): - SCENARIO0_DIR = os.path.join(TEST_DIR, "scenario_0") - geotif = os.path.join(SCENARIO0_DIR, 'small_dem.tif') + SCENARIO0_DIR = TEST_DIR / "scenario_0" + geotif = SCENARIO0_DIR / 'small_dem.tif' profile = rio_profile(geotif) data = rio_open(geotif) with pushd(tmp_path): - fname = os.path.join(tmp_path, 'tmp_file.nc') + path = tmp_path / 'tmp_file.nc' writeArrayToRaster( data, - fname, + path, proj=profile['crs'], gt=profile['transform'], fmt='nc', ) - new_fname = os.path.join(tmp_path, 'tmp_file.tif') - prof = rio_profile(new_fname) + new_path = tmp_path / 'tmp_file.tif' + prof = rio_profile(new_path) assert prof['driver'] == 'GTiff' diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index f980c47aa..bbf08f830 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -360,32 +360,33 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: # A dataset was returned by the above # Dataset returned: Cube e.g. GUNW workflow if hydro_delay is None: - out_filename = Path(out_filename.replace('wet', 'tropo')) + out_path = Path(out_filename.replace('wet', 'tropo')) ds = wet_delay - ext = out_filename.suffix + ext = out_path.suffix # data provenance: include metadata for model and times used times_str = [t.strftime('%Y%m%dT%H:%M:%S') for t in sorted(times)] ds = ds.assign_attrs(model_name=model._Name, model_times_used=times_str, interpolation_method=interp_method) if ext not in ('.nc', '.h5'): - out_filename = Path(out_filename.stem + '.nc') + out_path = Path(out_path.stem + '.nc') - if out_filename.suffix == '.nc': - ds.to_netcdf(out_filename, mode='w') - elif out_filename.suffix == '.h5': - ds.to_netcdf(out_filename, engine='h5netcdf', invalid_netcdf=True) + if out_path.suffix == '.nc': + ds.to_netcdf(out_path, mode='w') + elif out_path.suffix == '.h5': + ds.to_netcdf(out_path, engine='h5netcdf', invalid_netcdf=True) - logger.info('\nSuccessfully wrote delay cube to: %s\n', out_filename) + logger.info('\nSuccessfully wrote delay cube to: %s\n', out_path) # Dataset returned: station files, radar_raster, geocoded_file else: - out_filename = Path(out_filename) + out_path = Path(out_filename) + hydro_path = Path(hydro_filename) if aoi.type() == 'station_file': - out_filename = out_filename.with_suffix('.csv') + out_path = out_path.with_suffix('.csv') if aoi.type() in ('station_file', 'radar_rasters', 'geocoded_file'): - writeDelays(aoi, wet_delay, hydro_delay, out_filename, Path(hydro_filename), outformat=run_config.runtime_group.raster_format) + writeDelays(aoi, wet_delay, hydro_delay, out_path, hydro_path, outformat=run_config.runtime_group.raster_format) - wet_paths.append(out_filename) + wet_paths.append(out_path) return wet_paths diff --git a/tools/RAiDER/dem.py b/tools/RAiDER/dem.py index c6d2f116a..b1bfe57de 100644 --- a/tools/RAiDER/dem.py +++ b/tools/RAiDER/dem.py @@ -6,6 +6,7 @@ # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import os +from pathlib import Path import numpy as np import rasterio @@ -17,7 +18,7 @@ def download_dem( ll_bounds=None, - demName='warpedDEM.dem', + dem_path: Path=Path('warpedDEM.dem'), overwrite=False, writeDEM=False, buf=0.02, @@ -35,20 +36,17 @@ def download_dem( zvals: np.array -DEM heights metadata: -metadata for the DEM """ - if os.path.exists(demName): - if overwrite: - download = True - else: - download = False + if dem_path.exists(): + download = overwrite else: download = True - if download and (ll_bounds is None): + if download and ll_bounds is None: raise ValueError('download_dem: Either an existing file or lat/lon bounds must be passed') if not download: - logger.info('Using existing DEM: %s', demName) - zvals, metadata = rio_open(demName, returnProj=True) + logger.info('Using existing DEM: %s', dem_path) + zvals, metadata = rio_open(dem_path, returnProj=True) else: # download the dem # inExtent is SNWE @@ -67,9 +65,9 @@ def download_dem( dst_area_or_point='Area', ) if writeDEM: - with rasterio.open(demName, 'w', **metadata) as ds: + with rasterio.open(dem_path, 'w', **metadata) as ds: ds.write(zvals, 1) ds.update_tags(AREA_OR_POINT='Point') - logger.info('Wrote DEM: %s', demName) + logger.info('Wrote DEM: %s', dem_path) return zvals, metadata diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index a89c01712..0dcaa4691 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -222,7 +222,7 @@ def readZ(self): download_dem( self._bounding_box, writeDEM=True, - demName=demFile, + dem_path=demFile, ) ## interpolate the DEM to the query points @@ -293,7 +293,7 @@ def readZ(self): download_dem( self._bounding_box, writeDEM=True, - demName=demFile, + dem_path=demFile, ) z_out = interpolateDEM(demFile, self.readLL()) @@ -350,7 +350,7 @@ def readZ(self): demFile = self._filename if self._is_dem else 'GLO30_fullres_dem.tif' bbox = self._bounding_box - _, _ = download_dem(bbox, writeDEM=True, demName=demFile) + _, _ = download_dem(bbox, writeDEM=True, dem_path=demFile) z_out = interpolateDEM(demFile, self.readLL()) return z_out diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 4510f3ba0..d843aef1b 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -234,7 +234,7 @@ def get_file_and_band(filestr: str) -> tuple[Path, 1]: raise ValueError(f'Cannot interpret {filestr} as valid filename') -def writeArrayToRaster(array, filename, noDataValue=0.0, fmt='ENVI', proj=None, gt=None) -> None: +def writeArrayToRaster(array, path: Path, noDataValue=0.0, fmt='ENVI', proj=None, gt=None) -> None: """Write a numpy array to a GDAL-readable raster.""" array_shp = np.shape(array) if array.ndim != 2: @@ -259,10 +259,10 @@ def writeArrayToRaster(array, filename, noDataValue=0.0, fmt='ENVI', proj=None, # cant write netcdfs with rasterio in a simple way if fmt == 'nc': fmt = 'GTiff' - filename = filename.replace('.nc', '.tif') + path = path.with_suffix('.tif') with rasterio.open( - filename, + path, mode='w', count=1, width=array_shp[1], @@ -274,7 +274,7 @@ def writeArrayToRaster(array, filename, noDataValue=0.0, fmt='ENVI', proj=None, transform=trans, ) as dst: dst.write(array, 1) - logger.info('Wrote: %s', filename) + logger.info('Wrote: %s', path) def round_date(date, precision): @@ -412,7 +412,7 @@ def writeDelays( wetDelay, hydroDelay, wet_path: Path, - hydro_filename: Optional[str]=None, + hydro_path: Optional[Path]=None, outformat: str=None, ndv: float=0.0 ) -> None: @@ -435,10 +435,12 @@ def writeDelays( logger.info('Wrote delays to: %s', wet_path.absolute()) else: + if hydro_path is None: + raise ValueError('Hydro delay file path must be specified if the AOI is not a station file') proj = aoi.projection() gt = aoi.geotransform() - writeArrayToRaster(wetDelay, str(wet_path), noDataValue=ndv, fmt=outformat, proj=proj, gt=gt) - writeArrayToRaster(hydroDelay, hydro_filename, noDataValue=ndv, fmt=outformat, proj=proj, gt=gt) + writeArrayToRaster(wetDelay, wet_path, noDataValue=ndv, fmt=outformat, proj=proj, gt=gt) + writeArrayToRaster(hydroDelay, hydro_path, noDataValue=ndv, fmt=outformat, proj=proj, gt=gt) def getTimeFromFile(filename): From ba37429e32da922776d022e90f28faa0403f620e Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:43:01 -0500 Subject: [PATCH 46/76] Fix typo --- tools/RAiDER/utilFcns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index d843aef1b..b270bb3e6 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -221,7 +221,7 @@ def rio_stats(path: Path, band=1): return stats, proj, gt -def get_file_and_band(filestr: str) -> tuple[Path, 1]: +def get_file_and_band(filestr: str) -> tuple[Path, int]: """Support file;bandnum as input for filename strings.""" parts = filestr.split(';') From df3c4935a5a9ad7a89f40f5ab5559689eb8bfc75 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:45:32 -0500 Subject: [PATCH 47/76] Add type annotations Type annotations for this rio_stats required inventing a few new types. The new types are my inferences based on looking at rasterio's output in the debugger. For some reason rasterio doesn't export these symbols so I can't use the real ones in type annotations. Hopefully my guesses at what they are aren't wrong. --- tools/RAiDER/llreader.py | 13 +++++-------- tools/RAiDER/utilFcns.py | 6 +++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index 0dcaa4691..a144624e6 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -386,23 +386,20 @@ def readZ(self): return heights -def bounds_from_latlon_rasters(latfile, lonfile): +def bounds_from_latlon_rasters(lat_filestr: str, lon_filestr: str) -> tuple[BB.SNWE, CRS, RIO.GDAL]: """ Parse lat/lon/height inputs and return the appropriate outputs. """ from RAiDER.utilFcns import get_file_and_band - latinfo = get_file_and_band(str(latfile)) - loninfo = get_file_and_band(str(lonfile)) + latinfo = get_file_and_band(lat_filestr) + loninfo = get_file_and_band(lon_filestr) lat_stats, lat_proj, lat_gt = rio_stats(latinfo[0], band=latinfo[1]) lon_stats, lon_proj, lon_gt = rio_stats(loninfo[0], band=loninfo[1]) - if lat_proj != lon_proj: - raise ValueError('Projection information for Latitude and Longitude files does not match') - - if lat_gt != lon_gt: - raise ValueError('Affine transform for Latitude and Longitude files does not match') + assert lat_proj == lon_proj, 'Projection information for Latitude and Longitude files does not match' + assert lat_gt == lon_gt, 'Affine transform for Latitude and Longitude files does not match' # TODO - handle dateline crossing here snwe = (lat_stats.min, lat_stats.max, lon_stats.min, lon_stats.max) diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index b270bb3e6..4a4f7a6a6 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -192,7 +192,7 @@ def nodataToNan(inarr, listofvals) -> None: inarr[inarr == val] = np.nan -def rio_stats(path: Path, band=1): +def rio_stats(path: Path, band: int=1) -> tuple[RIO.Statistics, Optional[CRS], RIO.GDAL]: """ Read a rasterio-compatible file and pull the metadata. @@ -214,9 +214,9 @@ def rio_stats(path: Path, band=1): # Turn off PAM to avoid creating .aux.xml files with rasterio.Env(GDAL_PAM_ENABLED='NO'): with rasterio.open(path) as src: - gt = src.transform.to_gdal() - proj = src.crs stats = src.statistics(band) + proj = src.crs + gt = src.transform.to_gdal() return stats, proj, gt From 7b0f8fe59ad790ff8e381699069b895ab1a228b7 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:45:46 -0500 Subject: [PATCH 48/76] Update __init__.py --- tools/RAiDER/types/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/RAiDER/types/__init__.py b/tools/RAiDER/types/__init__.py index 281b0598a..a16cab5f3 100644 --- a/tools/RAiDER/types/__init__.py +++ b/tools/RAiDER/types/__init__.py @@ -20,5 +20,13 @@ class CalcDelaysArgsUnparsed(argparse.Namespace): interpolate_time: TimeInterpolationMethod output_directory: Path -class CalcDelaysArgs(CalcDelaysArgsUnparsed): +class CalcDelaysArgs(argparse.Namespace): + bucket: Optional[str] + bucket_prefix: Optional[str] + input_bucket_prefix: Optional[str] file: Path + weather_model: str + api_uid: Optional[str] + api_key: Optional[str] + interpolate_time: TimeInterpolationMethod + output_directory: Path From 926fa91d82ffdbf2564f566f640afde6750f7cbf Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:02:26 -0500 Subject: [PATCH 49/76] Formatting --- tools/RAiDER/llreader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index a144624e6..5dd61afc2 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -402,7 +402,8 @@ def bounds_from_latlon_rasters(lat_filestr: str, lon_filestr: str) -> tuple[BB.S assert lat_gt == lon_gt, 'Affine transform for Latitude and Longitude files does not match' # TODO - handle dateline crossing here - snwe = (lat_stats.min, lat_stats.max, lon_stats.min, lon_stats.max) + snwe = (lat_stats.min, lat_stats.max, + lon_stats.min, lon_stats.max) if lat_proj is None: logger.debug('Assuming lat/lon files are in EPSG:4326') From cb938f772b81b2f9fee42be6f266746c4cea32c4 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:02:39 -0500 Subject: [PATCH 50/76] Update docstring --- tools/RAiDER/utilFcns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 4a4f7a6a6..25f6cecc4 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -197,7 +197,7 @@ def rio_stats(path: Path, band: int=1) -> tuple[RIO.Statistics, Optional[CRS], R Read a rasterio-compatible file and pull the metadata. Args: - fname - file path to be loaded + path - file path to be loaded band - band number to use for getting statistics Returns: From 78a2f451f027c31a1066fb7396c17a83da81a6bc Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 15:03:27 -0500 Subject: [PATCH 51/76] Fix bug from transition to Path Path was coming out to `file.rdr/.vrt` instead of `file.rdr.vrt` --- tools/RAiDER/utilFcns.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 25f6cecc4..c9ab6f929 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -208,8 +208,9 @@ def rio_stats(path: Path, band: int=1) -> tuple[RIO.Statistics, Optional[CRS], R if path.name.startswith('S1-GUNW'): path = Path(f'NETCDF:"{path}":science/grids/data/unwrappedPhase') - if (path / '.vrt').exists(): - path = path / '.vrt' + vrt_path = path.with_suffix(path.suffix + '.vrt') + if vrt_path.exists(): + path = vrt_path # Turn off PAM to avoid creating .aux.xml files with rasterio.Env(GDAL_PAM_ENABLED='NO'): From 35d09f686d2222f938a26fe877d8c775b226c3dc Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:17:02 -0500 Subject: [PATCH 52/76] Use Path --- test/test_dem.py | 11 ++++++----- tools/RAiDER/dem.py | 9 ++++----- tools/RAiDER/llreader.py | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/test/test_dem.py b/test/test_dem.py index 2ddf68d67..97fc06e44 100644 --- a/test/test_dem.py +++ b/test/test_dem.py @@ -1,4 +1,3 @@ -import os import pytest from test import TEST_DIR, pushd @@ -12,6 +11,7 @@ def test_download_dem_1(): overwrite=False ) assert hts.shape == (45,226) + assert meta is not None assert meta['crs'] is None @@ -22,17 +22,18 @@ def test_download_dem_2(): def test_download_dem_3(tmp_path): with pushd(tmp_path): - fname = os.path.join(tmp_path, 'tmp_file.nc') + path = tmp_path / 'tmp_file.nc' with pytest.raises(ValueError): - download_dem(dem_path=fname) + download_dem(dem_path=path) @pytest.mark.long def test_download_dem_4(tmp_path): with pushd(tmp_path): - fname = os.path.join(tmp_path, 'tmp_file.nc') - z,m = download_dem(dem_path=fname, overwrite=True, ll_bounds=[37.9,38.,-91.8,-91.7], writeDEM=True) + path = tmp_path / 'tmp_file.nc' + z, m = download_dem(dem_path=path, overwrite=True, ll_bounds=[37.9,38.,-91.8,-91.7], writeDEM=True) assert len(z.shape) == 2 + assert m is not None assert 'crs' in m.keys() diff --git a/tools/RAiDER/dem.py b/tools/RAiDER/dem.py index b1bfe57de..336142a8d 100644 --- a/tools/RAiDER/dem.py +++ b/tools/RAiDER/dem.py @@ -19,12 +19,11 @@ def download_dem( ll_bounds=None, dem_path: Path=Path('warpedDEM.dem'), - overwrite=False, - writeDEM=False, - buf=0.02, -): - """ Download a DEM if one is not already present. + overwrite: bool=False, + writeDEM: bool=False, + buf: float=0.02, +) -> tuple[np.ndarray, Optional[RIO.Profile]]: Args: llbounds: list/ndarry of floats -lat/lon bounds of the area to download. Values should be ordered in the following way: [S, N, W, E] diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index 5dd61afc2..194a252cb 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -222,7 +222,7 @@ def readZ(self): download_dem( self._bounding_box, writeDEM=True, - dem_path=demFile, + dem_path=Path(demFile), ) ## interpolate the DEM to the query points @@ -293,7 +293,7 @@ def readZ(self): download_dem( self._bounding_box, writeDEM=True, - dem_path=demFile, + dem_path=Path(demFile), ) z_out = interpolateDEM(demFile, self.readLL()) @@ -350,7 +350,7 @@ def readZ(self): demFile = self._filename if self._is_dem else 'GLO30_fullres_dem.tif' bbox = self._bounding_box - _, _ = download_dem(bbox, writeDEM=True, dem_path=demFile) + _, _ = download_dem(bbox, writeDEM=True, dem_path=Path(demFile)) z_out = interpolateDEM(demFile, self.readLL()) return z_out From 4aae9700b9ec6ef93a5d5748a0b97e17757557aa Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:17:39 -0500 Subject: [PATCH 53/76] Update dem.py --- tools/RAiDER/dem.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tools/RAiDER/dem.py b/tools/RAiDER/dem.py index 336142a8d..e75b70db1 100644 --- a/tools/RAiDER/dem.py +++ b/tools/RAiDER/dem.py @@ -5,35 +5,36 @@ # RESERVED. United States Government Sponsorship acknowledged. # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -import os from pathlib import Path +from typing import Optional import numpy as np import rasterio from dem_stitcher.stitcher import stitch_dem from RAiDER.logger import logger +from RAiDER.types import RIO from RAiDER.utilFcns import rio_open def download_dem( ll_bounds=None, dem_path: Path=Path('warpedDEM.dem'), - Download a DEM if one is not already present. overwrite: bool=False, writeDEM: bool=False, buf: float=0.02, ) -> tuple[np.ndarray, Optional[RIO.Profile]]: + """Download a DEM if one is not already present. Args: - llbounds: list/ndarry of floats -lat/lon bounds of the area to download. Values should be ordered in the following way: [S, N, W, E] - writeDEM: boolean -write the DEM to file - outName: string -name of the DEM file - buf: float -buffer to add to the bounds - overwrite: boolean -overwrite existing DEM + llbounds: list/ndarry of floats - lat/lon bounds of the area to download. Values should be ordered in the following way: [S, N, W, E] + writeDEM: bool - write the DEM to file + outName: string - name of the DEM file + buf: float - buffer to add to the bounds + overwrite: bool - overwrite existing DEM Returns: - zvals: np.array -DEM heights - metadata: -metadata for the DEM + zvals: np.array - DEM heights + metadata: - metadata for the DEM """ if dem_path.exists(): download = overwrite From a2ea30150477da093d99f7a5d65999ddd758e20c Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:44:32 -0500 Subject: [PATCH 54/76] Add type annotations --- tools/RAiDER/interpolator.py | 6 +++-- tools/RAiDER/utilFcns.py | 48 +++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/tools/RAiDER/interpolator.py b/tools/RAiDER/interpolator.py index a7f1c77b8..c69487e84 100644 --- a/tools/RAiDER/interpolator.py +++ b/tools/RAiDER/interpolator.py @@ -5,6 +5,8 @@ # RESERVED. United States Government Sponsorship acknowledged. # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +from pathlib import Path +from typing import Union import numpy as np import pandas as pd from scipy.interpolate import interp1d @@ -117,7 +119,7 @@ def fillna3D(array, axis=-1, fill_value=0.0): return outmat -def interpolateDEM(demFile, outLL, method='nearest'): +def interpolateDEM(dem_path: Union[Path, str], outLL: tuple[np.ndarray, np.ndarray], method='nearest') -> np.ndarray: """Interpolate a DEM raster to a set of lat/lon query points using rioxarray. outLL will be a tuple of (lats, lons). lats/lons can either be 1D arrays or 2 @@ -129,5 +131,5 @@ def interpolateDEM(demFile, outLL, method='nearest'): lats, lons = outLL lats = lats[:, 0] if lats.ndim == 2 else lats lons = lons[0, :] if lons.ndim == 2 else lons - z_out = da_dem.interp(y=np.sort(lats)[::-1], x=lons).data + z_out: np.ndarray = da_dem.interp(y=np.sort(lats)[::-1], x=lons).data return z_out diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index c9ab6f929..2b5680c9c 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -145,25 +145,31 @@ def rio_extents(profile: RIO.Profile) -> BB.SNWE: return S, N, W, E -def rio_open(path: Path, returnProj=False, userNDV=None, band=None): +def rio_open( + path: Path, + returnProj: bool=False, + userNDV: Optional[float]=None, + band: Optional[int]=None +) -> tuple[np.ndarray, Optional[RIO.Profile]]: """Reads a rasterio-compatible raster file and returns the data and profile.""" - if (path / '.vrt').exists(): - path /= '.vrt' + vrt_path = path.with_suffix(path.suffix + '.vrt') + if vrt_path.exists(): + path = vrt_path with rasterio.open(path) as src: - profile = src.profile + profile: Optional[RIO.Profile] = src.profile if returnProj else None # For all bands - nodata = src.nodatavals + nodata: tuple[float, ...] = src.nodatavals # If user requests a band if band is not None: ndv = nodata[band - 1] - data = src.read(band).squeeze() + data: np.ndarray = src.read(band).squeeze() nodataToNan(data, [userNDV, ndv]) else: - data = src.read().squeeze() + data: np.ndarray = src.read().squeeze() if data.ndim > 2: for bnd in range(data.shape[0]): val = data[bnd, ...] @@ -172,29 +178,24 @@ def rio_open(path: Path, returnProj=False, userNDV=None, band=None): nodataToNan(data, list(nodata) + [userNDV]) if data.ndim > 2: - dlist = [] + dlist: list[list[float]] = [] for k in range(data.shape[0]): - dlist.append(data[k, ...].copy()) - data = dlist + dlist.append(data[k].copy()) + data = np.array(dlist) - if not returnProj: - return data + return data, profile - else: - return data, profile - -def nodataToNan(inarr, listofvals) -> None: +def nodataToNan(inarr: np.ndarray, vals: list[Optional[float]]) -> None: """Setting values to nan as needed.""" inarr = inarr.astype(float) # nans cannot be integers (i.e. in DEM) - for val in listofvals: + for val in vals: if val is not None: inarr[inarr == val] = np.nan def rio_stats(path: Path, band: int=1) -> tuple[RIO.Statistics, Optional[CRS], RIO.GDAL]: - """ - Read a rasterio-compatible file and pull the metadata. + """Read a rasterio-compatible file and pull the metadata. Args: path - file path to be loaded @@ -235,7 +236,14 @@ def get_file_and_band(filestr: str) -> tuple[Path, int]: raise ValueError(f'Cannot interpret {filestr} as valid filename') -def writeArrayToRaster(array, path: Path, noDataValue=0.0, fmt='ENVI', proj=None, gt=None) -> None: +def writeArrayToRaster( + array: np.ndarray, + path: Path, + noDataValue: float=0.0, + fmt: str='ENVI', + proj: Optional[CRS]=None, + gt: Optional[RIO.GDAL]=None +) -> None: """Write a numpy array to a GDAL-readable raster.""" array_shp = np.shape(array) if array.ndim != 2: From 4792b1c861173e42f6c22cfc5f64020ecec8289b Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:44:39 -0500 Subject: [PATCH 55/76] Add None check --- tools/RAiDER/interpolator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/RAiDER/interpolator.py b/tools/RAiDER/interpolator.py index c69487e84..08ed2218c 100644 --- a/tools/RAiDER/interpolator.py +++ b/tools/RAiDER/interpolator.py @@ -126,8 +126,11 @@ def interpolateDEM(dem_path: Union[Path, str], outLL: tuple[np.ndarray, np.ndarr For now will only use first row/col of 2D """ import rioxarray as xrr + from xarray import Dataset - da_dem = xrr.open_rasterio(demFile, band_as_variable=True)['band_1'] + data = xrr.open_rasterio(dem_path, band_as_variable=True) + assert isinstance(data, Dataset), 'DEM could not be opened as a rioxarray dataset' + da_dem = data['band_1'] lats, lons = outLL lats = lats[:, 0] if lats.ndim == 2 else lats lons = lons[0, :] if lons.ndim == 2 else lons From 83e95969261b90009271e2354731191dc3341b7b Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:44:48 -0500 Subject: [PATCH 56/76] Use Path --- test/test_interpolator.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/test_interpolator.py b/test/test_interpolator.py index 7799967a0..a13c573b1 100644 --- a/test/test_interpolator.py +++ b/test/test_interpolator.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import numpy as np import rasterio as rio import pytest @@ -936,19 +937,19 @@ def test_interpolateDEM(): metadata = {'driver': 'GTiff', 'dtype': 'float32', 'width': s, 'height': s, 'count': 1} - demFile = './dem_tmp.tif' + dem_file = Path('./dem_tmp.tif') - with rio.open(demFile, 'w', **metadata) as ds: + with rio.open(dem_file, 'w', **metadata) as ds: ds.write(dem, 1) ds.update_tags(AREA_OR_POINT='Point') ## random points to interpolate to lons = np.array([4.5, 9.5]) lats = np.array([2.5, 9.5]) - out = interpolateDEM(demFile, (lats, lons)) + out = interpolateDEM(dem_file, (lats, lons)) gold = np.array([[36, 81], [8, 18]], dtype=float) assert np.allclose(out, gold) - os.remove(demFile) + dem_file.unlink() # TODO: implement an interpolator test that is similar to test_scenario_1. # Currently the scipy and C++ interpolators differ on that case. From 9cf4208e71859fbd74980d5a3cb833003bb4e843 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:24:11 -0500 Subject: [PATCH 57/76] Fix unmatched brackets error in Python 3.9 It seems Python <=3.9 cannot handle an object being subscripted with the same kind of quotes as the string it lies in, and this was causing runtime errors. So this changes out the inner quotes here for double quotes. --- tools/RAiDER/cli/raider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index bbf08f830..029f0ac78 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -100,7 +100,7 @@ def read_run_config_file(path: Path) -> RunConfig: # Validate look direction if not isinstance(yaml_data['look_dir'], str) or yaml_data['look_dir'].lower() not in ('right', 'left'): - raise ValueError(f'Unknown look direction {yaml_data['look_dir']}') + raise ValueError(f'Unknown look direction {yaml_data["look_dir"]}') # Support for deprecated location for cube_spacing_in_m if 'cube_spacing_in_m' in yaml_data: From db0a34564553cf0d44034bb1769046c7518b5826 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:27:04 -0500 Subject: [PATCH 58/76] Do not raise if cleanup fails These files are decidedly not safe when run in parallel. A bigger issue, but for now we will just make sure failing to remove these files because a different run already did does not fail the test. --- test/test_synthetic.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test_synthetic.py b/test/test_synthetic.py index 537710f56..d9574416a 100644 --- a/test/test_synthetic.py +++ b/test/test_synthetic.py @@ -1,3 +1,4 @@ +from pathlib import Path from RAiDER.cli.raider import calcDelays import pytest @@ -395,7 +396,7 @@ def test_wet_eq_nonlinear(region, mod='ERA-5'): np.testing.assert_almost_equal(0, resid, decimal=6) da.close() - os.remove('./temp.yaml') - os.remove('./error.log') - os.remove('./debug.log') + Path('./temp.yaml').unlink(missing_ok=True) + Path('./error.log').unlink(missing_ok=True) + Path('./debug.log').unlink(missing_ok=True) del da From 3ebaf1bc895c692dbc894b7a8b913280da95169b Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:28:52 -0500 Subject: [PATCH 59/76] rio_open: always return profile --- test/test_llreader.py | 4 ++-- test/test_util.py | 8 ++++---- tools/RAiDER/dem.py | 2 +- tools/RAiDER/llreader.py | 14 ++++++++------ tools/RAiDER/losreader.py | 3 ++- tools/RAiDER/utilFcns.py | 5 ++--- 6 files changed, 19 insertions(+), 17 deletions(-) diff --git a/test/test_llreader.py b/test/test_llreader.py index 6c9cc9801..b22ad3a73 100644 --- a/test/test_llreader.py +++ b/test/test_llreader.py @@ -71,8 +71,8 @@ def test_set_xygrid(): def test_latlon_reader(): latfile = Path(GEOM_DIR) / 'lat.rdr' lonfile = Path(GEOM_DIR) / 'lon.rdr' - lat_true = rio_open(latfile) - lon_true = rio_open(lonfile) + lat_true, _ = rio_open(latfile) + lon_true, _ = rio_open(lonfile) query = RasterRDR(lat_file=str(latfile), lon_file=str(lonfile)) lats, lons = query.readLL() diff --git a/test/test_util.py b/test/test_util.py index 7943aaf59..1bc3bdd65 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -122,7 +122,7 @@ def test_cosd(): def test_rio_open(): - out = rio_open(TEST_DIR / "test_geom/lat.rdr", False) + out, _ = rio_open(TEST_DIR / "test_geom/lat.rdr", False) assert np.allclose(out.shape, (45, 226)) @@ -162,7 +162,7 @@ def test_writeArrayToRaster_4(tmp_path): SCENARIO0_DIR = TEST_DIR / "scenario_0" geotif = SCENARIO0_DIR / 'small_dem.tif' profile = rio_profile(geotif) - data = rio_open(geotif) + data, _ = rio_open(geotif) with pushd(tmp_path): path = tmp_path / 'tmp_file.nc' writeArrayToRaster( @@ -537,14 +537,14 @@ def test_rio_2(): def test_rio_3(): geotif = SCENARIO0_DIR / 'small_dem.tif' - data = rio_open(geotif, returnProj=False, userNDV=None, band=1) + data, _ = rio_open(geotif, userNDV=None, band=1) assert data.shape == (569,558) def test_rio_4(): SCENARIO_DIR = TEST_DIR / "scenario_4" los_path = SCENARIO_DIR / 'los.rdr' - inc, hd = rio_open(los_path, returnProj=False) + inc, hd = rio_open(los_path) assert len(inc.shape) == 2 assert len(hd.shape) == 2 diff --git a/tools/RAiDER/dem.py b/tools/RAiDER/dem.py index e75b70db1..d9db6793a 100644 --- a/tools/RAiDER/dem.py +++ b/tools/RAiDER/dem.py @@ -46,7 +46,7 @@ def download_dem( if not download: logger.info('Using existing DEM: %s', dem_path) - zvals, metadata = rio_open(dem_path, returnProj=True) + zvals, metadata = rio_open(dem_path) else: # download the dem # inExtent is SNWE diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index 194a252cb..906bacc47 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -264,20 +264,22 @@ def __init__(self, lat_file, lon_file=None, hgt_file=None, dem_file=None, conven self._demfile = dem_file self._convention = convention - def readLL(self): + def readLL(self) -> tuple[np.ndarray, Optional[np.ndarray]]: # allow for 2-band lat/lon raster - lats = rio_open(Path(self._latfile)) + lats, _ = rio_open(Path(self._latfile)) if self._lonfile is None: - return lats + return lats, None else: - return lats, rio_open(Path(self._lonfile)) + lons, _ = rio_open(Path(self._lonfile)) + return lats, lons - def readZ(self): + def readZ(self) -> np.ndarray: """Read the heights from the raster file, or download a DEM if not present.""" if self._hgtfile is not None and os.path.exists(self._hgtfile): logger.info('Using existing heights at: %s', self._hgtfile) - return rio_open(self._hgtfile) + hgts, _ = rio_open(self._hgtfile) + return hgts else: # Download the DEM diff --git a/tools/RAiDER/losreader.py b/tools/RAiDER/losreader.py index 07b88fc3c..60b1216c0 100644 --- a/tools/RAiDER/losreader.py +++ b/tools/RAiDER/losreader.py @@ -116,7 +116,8 @@ def __call__(self, delays): try: # if an ISCE-style los file is passed open it with GDAL - LOS_enu = inc_hd_to_enu(*rio_open(self._file)) + data, _ = rio_open(self._file) + LOS_enu = inc_hd_to_enu(*data) except (OSError, TypeError): # Otherwise, treat it as an orbit / statevector file diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 2b5680c9c..51dcf43c8 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -147,17 +147,16 @@ def rio_extents(profile: RIO.Profile) -> BB.SNWE: def rio_open( path: Path, - returnProj: bool=False, userNDV: Optional[float]=None, band: Optional[int]=None -) -> tuple[np.ndarray, Optional[RIO.Profile]]: +) -> tuple[np.ndarray, RIO.Profile]: """Reads a rasterio-compatible raster file and returns the data and profile.""" vrt_path = path.with_suffix(path.suffix + '.vrt') if vrt_path.exists(): path = vrt_path with rasterio.open(path) as src: - profile: Optional[RIO.Profile] = src.profile if returnProj else None + profile: RIO.Profile = src.profile # For all bands nodata: tuple[float, ...] = src.nodatavals From 27d06ba2fb38fa7a7d1e887b74d79151c245b262 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:29:06 -0500 Subject: [PATCH 60/76] Add type annotations --- tools/RAiDER/llreader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index 906bacc47..dabb63045 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -198,7 +198,7 @@ def __init__(self, station_file, demFile=None, cube_spacing_in_m: Optional[float self._bounding_box = bounds_from_csv(station_file) self._type = 'station_file' - def readLL(self): + def readLL(self) -> tuple[np.ndarray, np.ndarray]: """Read the station lat/lons from the csv file.""" df = pd.read_csv(self._filename).drop_duplicates(subset=['Lat', 'Lon']) return df['Lat'].values, df['Lon'].values @@ -334,7 +334,7 @@ def __init__(self, path: Path, is_dem=False, cube_spacing_in_m: Optional[float]= except KeyError: self.crs = None - def readLL(self): + def readLL(self) -> tuple[np.ndarray, np.ndarray]: # ll_bounds are SNWE S, N, W, E = self._bounding_box w, h = self.p['width'], self.p['height'] @@ -375,7 +375,7 @@ def get_extent(self): return [S, N, W, E] ## untested - def readLL(self): + def readLL(self) -> tuple[np.ndarray, np.ndarray]: with xarray.open_dataset(self.path) as ds: lats = ds.latitutde.data() lons = ds.longitude.data() From 10da5092b0991178c345ea409abf9023d4351cd9 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:29:17 -0500 Subject: [PATCH 61/76] Ensure readLL returns numpy arrays --- tools/RAiDER/llreader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index dabb63045..7056ad291 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -201,7 +201,7 @@ def __init__(self, station_file, demFile=None, cube_spacing_in_m: Optional[float def readLL(self) -> tuple[np.ndarray, np.ndarray]: """Read the station lat/lons from the csv file.""" df = pd.read_csv(self._filename).drop_duplicates(subset=['Lat', 'Lon']) - return df['Lat'].values, df['Lon'].values + return df['Lat'].to_numpy(), df['Lon'].to_numpy() def readZ(self): """Read the station heights from the file, or download a DEM if not present.""" From 726b8dac55c87e6074cc2b7d93d91fb70ccb9810 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:15:07 -0500 Subject: [PATCH 62/76] Extract parsing CRS to function --- tools/RAiDER/delay.py | 16 +++++++++++----- tools/RAiDER/types/__init__.py | 6 ++++-- tools/RAiDER/utilFcns.py | 12 +++++++++++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/tools/RAiDER/delay.py b/tools/RAiDER/delay.py index bf51e1998..47408982e 100755 --- a/tools/RAiDER/delay.py +++ b/tools/RAiDER/delay.py @@ -17,6 +17,8 @@ from datetime import datetime, timezone from typing import Optional, Union +from RAiDER.types import CRSLike +from RAiDER.utilFcns import parse_crs import numpy as np import pyproj import xarray @@ -397,7 +399,13 @@ def writeResultsToXarray(dt, xpts, ypts, zpts, crs, wetDelay, hydroDelay, weathe return ds -def transformPoints(lats: np.ndarray, lons: np.ndarray, hgts: np.ndarray, old_proj: CRS, new_proj: CRS) -> np.ndarray: +def transformPoints( + lats: np.ndarray, + lons: np.ndarray, + hgts: np.ndarray, + old_proj: CRSLike, + new_proj: CRSLike, +) -> np.ndarray: """ Transform lat/lon/hgt data to an array of points in a new projection. @@ -412,10 +420,8 @@ def transformPoints(lats: np.ndarray, lons: np.ndarray, hgts: np.ndarray, old_pr ndarray: the array of query points in the weather model coordinate system (YX) """ # Flags for flipping inputs or outputs - if not isinstance(new_proj, CRS): - new_proj = CRS.from_epsg(new_proj.lstrip('EPSG:')) - if not isinstance(old_proj, CRS): - old_proj = CRS.from_epsg(old_proj.lstrip('EPSG:')) + old_proj = parse_crs(old_proj) + new_proj = parse_crs(new_proj) t = Transformer.from_crs(old_proj, new_proj, always_xy=True) diff --git a/tools/RAiDER/types/__init__.py b/tools/RAiDER/types/__init__.py index a16cab5f3..6f8c80f9b 100644 --- a/tools/RAiDER/types/__init__.py +++ b/tools/RAiDER/types/__init__.py @@ -2,12 +2,14 @@ import argparse from pathlib import Path -from typing import Literal, Optional +from typing import Literal, Optional, Union +from pyproj import CRS -LookDir = Literal['right', 'left'] +LookDir = Literal['right', 'left'] TimeInterpolationMethod = Literal['none', 'center_time', 'azimuth_time_grid'] +CRSLike = Union[CRS, str, int] class CalcDelaysArgsUnparsed(argparse.Namespace): bucket: Optional[str] diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 51dcf43c8..58a653c30 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -22,7 +22,7 @@ _g1 as G1, ) from RAiDER.logger import logger -from RAiDER.types import BB, RIO +from RAiDER.types import BB, RIO, CRSLike # Optional imports @@ -905,3 +905,13 @@ def write_yaml(content: dict[str, Any], dst: Union[str, Path]) -> Path: logger.info('Wrote new cfg file: %s', str(dst)) return dst + + +def parse_crs(proj: CRSLike) -> CRS: + if isinstance(proj, CRS): + return proj + elif isinstance(proj, str): + return CRS.from_epsg(proj.lstrip('EPSG:')) + elif isinstance(proj, int): + return CRS.from_epsg(proj) + raise TypeError(f'Data type "{type(proj)}" not supported for CRS') From c0adcd224796c21aa9763b606143a97cbffb19c0 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:04:01 -0500 Subject: [PATCH 63/76] rio_open: always return profile --- test/test_util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_util.py b/test/test_util.py index 1bc3bdd65..ded21ed64 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -544,7 +544,8 @@ def test_rio_3(): def test_rio_4(): SCENARIO_DIR = TEST_DIR / "scenario_4" los_path = SCENARIO_DIR / 'los.rdr' - inc, hd = rio_open(los_path) + los, _ = rio_open(los_path) + inc, hd = los assert len(inc.shape) == 2 assert len(hd.shape) == 2 From 6005d1e84018d002e471d2ce1502d982c0df0df9 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 5 Aug 2024 14:39:35 -0500 Subject: [PATCH 64/76] Fix merge conflicts --- test/test_checkArgs.py | 84 ++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 52 deletions(-) diff --git a/test/test_checkArgs.py b/test/test_checkArgs.py index b9520e872..397d0aa7b 100644 --- a/test/test_checkArgs.py +++ b/test/test_checkArgs.py @@ -1,22 +1,21 @@ import datetime import os -from pathlib import Path import shutil -import pytest +from pathlib import Path import pandas as pd +import pytest -from test import TEST_DIR, pushd - +from RAiDER.checkArgs import checkArgs, get_raster_ext, makeDelayFileNames from RAiDER.cli.args import AOIGroup, DateGroup, HeightGroupUnparsed, LOSGroup, RunConfig, RuntimeGroup, TimeGroup -from RAiDER.checkArgs import checkArgs, makeDelayFileNames, get_raster_ext -from RAiDER.llreader import BoundingBox, StationFile, RasterRDR +from RAiDER.llreader import BoundingBox, RasterRDR, StationFile from RAiDER.losreader import Zenith from RAiDER.models.gmao import GMAO +from test import TEST_DIR, pushd -SCENARIO_1 = os.path.join(TEST_DIR, "scenario_1") -SCENARIO_2 = os.path.join(TEST_DIR, "scenario_2") +SCENARIO_1 = os.path.join(TEST_DIR, 'scenario_1') +SCENARIO_2 = os.path.join(TEST_DIR, 'scenario_2') @pytest.fixture(autouse=True) @@ -28,15 +27,16 @@ def args(): aoi_group=AOIGroup(aoi=BoundingBox([38, 39, -92, -91])), los_group=LOSGroup(los=Zenith()), height_group=HeightGroupUnparsed(), - runtime_group=RuntimeGroup() + runtime_group=RuntimeGroup(), ) - for f in "weather_files weather_dir".split(): - shutil.rmtree(f) if os.path.exists(f) else "" + for f in 'weather_files weather_dir'.split(): + shutil.rmtree(f) if os.path.exists(f) else '' return d + def isWriteable(dirpath: Path) -> bool: - """Test whether a directory is writeable""" + """Test whether a directory is writeable.""" try: with (dirpath / 'tmp.txt').open('w'): pass @@ -46,7 +46,6 @@ def isWriteable(dirpath: Path) -> bool: def test_checkArgs_outfmt_1(args): - """Test that passing height levels with hdf5 outformat works""" args.runtime_group.file_format = 'h5' args.height_group.height_levels = [10, 100, 1000] args = checkArgs(args) @@ -54,34 +53,27 @@ def test_checkArgs_outfmt_1(args): def test_checkArgs_outfmt_2(args): - """Test that passing a raster format with height levels throws an error""" args.runtime_group.file_format = 'GTiff' args.height_group.height_levels = [10, 100, 1000] args = checkArgs(args) - assert os.path.splitext(args.wetFilenames[0])[-1] == ".nc" + assert os.path.splitext(args.wetFilenames[0])[-1] == '.nc' def test_checkArgs_outfmt_3(args): - """Test that passing a raster format with height levels throws an error""" - args = args - '''Test that passing a raster format with height levels throws an error''' with pytest.raises(FileNotFoundError): args.aoi_group.aoi = StationFile(os.path.join('fake_dir', 'stations.csv')) def test_checkArgs_outfmt_4(args): - """Test that passing a raster format with height levels throws an error""" - args = args - args.aoi = RasterRDR( - lat_file=os.path.join(SCENARIO_1, "geom", "lat.dat"), - lon_file=os.path.join(SCENARIO_1, "geom", "lon.dat"), + args.aoi_group.aoi = RasterRDR( + lat_file=os.path.join(SCENARIO_1, 'geom', 'lat.dat'), + lon_file=os.path.join(SCENARIO_1, 'geom', 'lon.dat'), ) args = checkArgs(args) assert args.aoi_group.aoi.type() == 'radar_rasters' def test_checkArgs_outfmt_5(args, tmp_path): - '''Test that passing a raster format with height levels throws an error''' with pushd(tmp_path): args.aoi_group.aoi = StationFile(os.path.join(SCENARIO_2, 'stations.csv')) args = checkArgs(args) @@ -89,17 +81,17 @@ def test_checkArgs_outfmt_5(args, tmp_path): def test_checkArgs_outloc_1(args): - """Test that the default output and weather model directories are correct""" + """Test that the default output and weather model directories are correct.""" args = args argDict = checkArgs(args) out = argDict.runtime_group.output_directory wmLoc = argDict.runtime_group.weather_model_directory assert os.path.abspath(out) == os.getcwd() - assert os.path.abspath(wmLoc) == os.path.join(os.getcwd(), "weather_files") + assert os.path.abspath(wmLoc) == os.path.join(os.getcwd(), 'weather_files') def test_checkArgs_outloc_2(args, tmp_path): - """Tests that the correct output location gets assigned when provided""" + """Tests that the correct output location gets assigned when provided.""" with pushd(tmp_path): args.runtime_group.output_directory = tmp_path argDict = checkArgs(args) @@ -108,7 +100,7 @@ def test_checkArgs_outloc_2(args, tmp_path): def test_checkArgs_outloc_2b(args, tmp_path): - """Tests that the weather model directory gets passed through by itself""" + """Tests that the weather model directory gets passed through by itself.""" with pushd(tmp_path): args.runtime_group.output_directory = tmp_path wm_dir = Path('weather_dir') @@ -118,7 +110,7 @@ def test_checkArgs_outloc_2b(args, tmp_path): def test_checkArgs_outloc_3(args, tmp_path): - """Tests that the weather model directory gets created when needed""" + """Tests that the weather model directory gets created when needed.""" with pushd(tmp_path): args.runtime_group.output_directory = tmp_path argDict = checkArgs(args) @@ -126,7 +118,7 @@ def test_checkArgs_outloc_3(args, tmp_path): def test_checkArgs_outloc_4(args): - """Tests for creating writeable weather model directory""" + """Tests for creating writeable weather model directory.""" args = args argDict = checkArgs(args) @@ -134,7 +126,7 @@ def test_checkArgs_outloc_4(args): def test_filenames_1(args): - """tests that the correct filenames are generated""" + """tests that the correct filenames are generated.""" args = args argDict = checkArgs(args) assert 'Delay' not in argDict.wetFilenames[0] @@ -146,7 +138,7 @@ def test_filenames_1(args): def test_filenames_2(args): - '''Tests that the correct filenames are generated''' + """Tests that the correct filenames are generated.""" args.runtime_group.output_directory = Path(SCENARIO_2) args.aoi_group.aoi = StationFile(os.path.join(SCENARIO_2, 'stations.csv')) argDict = checkArgs(args) @@ -155,36 +147,24 @@ def test_filenames_2(args): def test_makeDelayFileNames_1(): - assert makeDelayFileNames(None, None, "h5", "name", Path("dir")) == ( - "dir/name_wet_ztd.h5", - "dir/name_hydro_ztd.h5" - ) + assert makeDelayFileNames(None, None, 'h5', 'name', Path('dir')) == ('dir/name_wet_ztd.h5', 'dir/name_hydro_ztd.h5') def test_makeDelayFileNames_2(): - assert makeDelayFileNames(None, (), "h5", "name", Path("dir")) == ( - "dir/name_wet_std.h5", - "dir/name_hydro_std.h5" - ) + assert makeDelayFileNames(None, (), 'h5', 'name', Path('dir')) == ('dir/name_wet_std.h5', 'dir/name_hydro_std.h5') def test_makeDelayFileNames_3(): - assert makeDelayFileNames(datetime.datetime(2020, 1, 1, 1, 2, 3), None, "h5", "model_name", Path("dir")) == ( - "dir/model_name_wet_20200101T010203_ztd.h5", - "dir/model_name_hydro_20200101T010203_ztd.h5" + assert makeDelayFileNames(datetime.datetime(2020, 1, 1, 1, 2, 3), None, 'h5', 'model_name', Path('dir')) == ( + 'dir/model_name_wet_20200101T010203_ztd.h5', + 'dir/model_name_hydro_20200101T010203_ztd.h5', ) def test_makeDelayFileNames_4(): - assert makeDelayFileNames( - datetime.datetime(1900, 12, 31, 1, 2, 3), - "los", - "h5", - "model_name", - Path("dir") - ) == ( - "dir/model_name_wet_19001231T010203_std.h5", - "dir/model_name_hydro_19001231T010203_std.h5" + assert makeDelayFileNames(datetime.datetime(1900, 12, 31, 1, 2, 3), 'los', 'h5', 'model_name', Path('dir')) == ( + 'dir/model_name_wet_19001231T010203_std.h5', + 'dir/model_name_hydro_19001231T010203_std.h5', ) From 37684c72e0327e3b3d42a26f0bb4b058f557e63a Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 5 Aug 2024 14:40:15 -0500 Subject: [PATCH 65/76] Adapt for tmp_path --- test/scenario_1/raider_example_1.yaml | 4 ---- test/test_HRRR_ztd.py | 10 ++++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/test/scenario_1/raider_example_1.yaml b/test/scenario_1/raider_example_1.yaml index 0d2febc09..7305a400d 100644 --- a/test/scenario_1/raider_example_1.yaml +++ b/test/scenario_1/raider_example_1.yaml @@ -11,7 +11,3 @@ height_group: height_levels: 0 50 100 500 1000 # Return only these specific height levels los_group: # absent other options ZTD is calculated - runtime_group: - output_directory: test/scenario_1 - - diff --git a/test/test_HRRR_ztd.py b/test/test_HRRR_ztd.py index 7dc7fdfad..4777de4ad 100644 --- a/test/test_HRRR_ztd.py +++ b/test/test_HRRR_ztd.py @@ -1,17 +1,19 @@ -from test import TEST_DIR +from test import TEST_DIR, pushd import numpy as np import xarray as xr from RAiDER.cli.raider import calcDelays -def test_scenario_1(data_for_hrrr_ztd, mocker): +def test_scenario_1(tmp_path, data_for_hrrr_ztd, mocker): SCENARIO_DIR = TEST_DIR / "scenario_1" test_path = SCENARIO_DIR / 'raider_example_1.yaml' mocker.patch('RAiDER.processWM.prepareWeatherModel', side_effect=[str(data_for_hrrr_ztd)]) - calcDelays([str(test_path)]) + + with pushd(tmp_path): + calcDelays([str(test_path)]) + new_data = xr.load_dataset('HRRR_tropo_20200101T120000_ztd.nc') - new_data = xr.load_dataset(SCENARIO_DIR / 'HRRR_tropo_20200101T120000_ztd.nc') new_data1 = new_data.sel(x=-91.84, y=36.84, z=0, method='nearest') golden_data = 2.2622863, 0.0361021 # hydro|wet From e68f5471f63ac95670f3a97959b1fd1b2c349e46 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 5 Aug 2024 14:40:46 -0500 Subject: [PATCH 66/76] Delete mistakenly pushed test output --- ...-5_2020_01_30_T13_52_45_14N_18N_102W_99W.nc | Bin 597308 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/weather_files/ERA-5_2020_01_30_T13_52_45_14N_18N_102W_99W.nc diff --git a/test/weather_files/ERA-5_2020_01_30_T13_52_45_14N_18N_102W_99W.nc b/test/weather_files/ERA-5_2020_01_30_T13_52_45_14N_18N_102W_99W.nc deleted file mode 100644 index 6e31b5f707feb34daad14827f139dfa2d8e99cca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 597308 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

OLH?Tn&$9}#$D_!dc(Z_ULVS-~#HJTk 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%0D)7YT44@nXcaV=7i`NG~x1YNx?A+wwo^ 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 z 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«{=T^8-(XIZ~Z$Diru~5R|FfLG^{^_$C*ej?87{&jXgEX7)y5WbDYBX z##QR3b&T5sb=9h2eD`?7M&Jrj%K7?2JhXXC8dwAwO}P%WL%Y|#=P5DoMSK6CHvBny zvVa9@3M+^_hHi{fekGX4XS4t3%)J#t_sX8?U$BlCb2P>A_vt3LufpH0^7{m#Ffmfh z3||rLA?IOZ)g?M%$gP-8n+qFU4xXRDdX~r?)P+zWbH`(D*e}!(F&Tj8)frhg>Ayl) zUB4%6j`6oQS)|iAPky}6nF0IBnc^?ZhLM($Mftd2XTBefRhV}=>qFUYI9#ZoHiGrz zcMQ_5V(evi_);OpOMc5PzUX9aO|F|^{U4_lV@7TC}*-($v0Y+t|0aF`wIs{0u$3Nf#&}os;=)@c>`~&xLh-bgqXpO1-NG!Y`dt*(MexbyHmJ*p5|L>9n>*Cmv z1$}DR-?71$QaAkjzbzAP%cpL#V!ZR9&{;|ttm1NY57au9eODRY{1l)b0{eC+#`?k! zpXeC3_fhL+7%`3YWh?t0M8khfC#J2TFHL02T^M{vnZXSvhn(umhx4QZY!i_4$OYL; zSpRmb?#eHWd9H;Tz20`vwxm}*U|FZ4pdt(!$T$tbbXk|cHiBt6Bci-ILE>$s#VEB z$iOnXmIdcT#)d1P`7YVAIwmjC&EFRXv1XP&CqDPijVHoCmWi~3o6GGmW9+NiCL=!g8J7!tnD9I%OV#OM=lS)lN*os$ zi_V{e4Ci;m{0I3irJtIGJ&k*o=i%|IId9Hke#qIGxI*}3OY6M|G_-x8If3(Y=9|4; zFt)SsKWBId|E|puX!RvzK^m6+ySASjMt+)qV+H-moesT$t@Ct~!;ntS_^uQr{JXChboUnwt zbv7n>@RV4k)AsfJx_C}jLWj#*G9Te*iRJQAxRHN<#%lXK_tbl1=<}gHY>NS3r$K+K zr{Gs9rSIMFOoM)TCJgZE`uq(R4fU{7VE$Vz9V$gAYkV_F3d&_389M}x!dEu;L4|(9 z&#y2aKmKX*8+w)hx9$$_S#&7+LVCyVsRoejkiCs1EY?qr-lE6%A6L7CVOXSmm4pY! zn@qnLE5jpsL&mz0=Vr`{ZR;1k&Q@a{Gf$BX3C682yS E)x-d$PPF|2DbRgGRXgd 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 Bf;a#G From db8de23b0245acf28dbcb71e2342b768d73a5e2a Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:07:21 -0500 Subject: [PATCH 67/76] Fix unmatched brackets error in Python <=3.11 --- tools/RAiDER/gnss/processDelayFiles.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/RAiDER/gnss/processDelayFiles.py b/tools/RAiDER/gnss/processDelayFiles.py index 2cda4c9a8..dca2ef4b9 100644 --- a/tools/RAiDER/gnss/processDelayFiles.py +++ b/tools/RAiDER/gnss/processDelayFiles.py @@ -179,8 +179,8 @@ def local_time_filter(raiderFile, ztdFile, dfr, dfz, localTime): # only keep observation closest to Localtime print( - f'Total number of datapoints dropped in {raiderFile} for not being within {localTime.split(' ')[1]} hrs of ' - f'specified local-time {localTime.split(' ')[0]}: {dfr.shape[0]} out of {OG_total}' + f'Total number of datapoints dropped in {raiderFile} for not being within {localTime.split(" ")[1]} hrs of ' + f'specified local-time {localTime.split(" ")[0]}: {dfr.shape[0]} out of {OG_total}' ) dfz['Localtime_u'] = dfz['Localtime'] + datetime.timedelta(hours=localTime_hrthreshold) dfz['Localtime_l'] = dfz['Localtime'] - datetime.timedelta(hours=localTime_hrthreshold) @@ -188,8 +188,8 @@ def local_time_filter(raiderFile, ztdFile, dfr, dfz, localTime): dfz = dfz[(dfz['Datetime'] >= dfz['Localtime_l']) & (dfz['Datetime'] <= dfz['Localtime_u'])] # only keep observation closest to Localtime print( - f'Total number of datapoints dropped in {ztdFile} for not being within {localTime.split(' ')[1]} hrs of ' - f'specified local-time {localTime.split(' ')[0]}: {dfz.shape[0]} out of {OG_total}' + f'Total number of datapoints dropped in {ztdFile} for not being within {localTime.split(" ")[1]} hrs of ' + f'specified local-time {localTime.split(" ")[0]}: {dfz.shape[0]} out of {OG_total}' ) # drop all lines with nans From 14043dcaf8ef324dc08a9cc0f3fb120547aa159d Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Mon, 5 Aug 2024 15:18:27 -0500 Subject: [PATCH 68/76] Don't use Self in Python <3.11 --- tools/RAiDER/cli/validators.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tools/RAiDER/cli/validators.py b/tools/RAiDER/cli/validators.py index 220e5576c..36d3d9cfa 100755 --- a/tools/RAiDER/cli/validators.py +++ b/tools/RAiDER/cli/validators.py @@ -1,17 +1,21 @@ import argparse import datetime as dt import importlib -import itertools import re -import time +import sys from pathlib import Path -from typing import Any, Optional, Self, Union +from typing import Any, Optional, Union import numpy as np import pandas as pd + +if sys.version_info >= (3,11): + from typing import Self +else: + Self = Any + from RAiDER.cli.args import ( - AOIGroup, AOIGroupUnparsed, DateGroup, DateGroupUnparsed, From b530460fb61bb3770e6adcda0dc1051b912ac64f Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:16:54 -0500 Subject: [PATCH 69/76] Use getattr instead of eval General practice highly recommends against the use of eval when at all possible for security reasons: https://www.codiga.io/blog/python-eval/. I found that getattr could do what eval was meant to do here. --- tools/RAiDER/cli/statsPlot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/RAiDER/cli/statsPlot.py b/tools/RAiDER/cli/statsPlot.py index 7a47b08ac..73bc3d929 100755 --- a/tools/RAiDER/cli/statsPlot.py +++ b/tools/RAiDER/cli/statsPlot.py @@ -405,7 +405,8 @@ def convert_SI(val, unit_in, unit_out): # adjust if input isn't datetime, and assume it to be part of workflow # e.g. sigZTD filter, already extracted datetime object try: - return eval(f'val.apply(pd.to_datetime).dt.{unit_out}.astype(float).astype("Int32")') + datetime = val.apply(pd.to_datetime).dt + return getattr(datetime, unit_out).astype(float).astype("Int32") except AttributeError: return val From 1d184b5511c77bf8001cf1acab6ea924466b527b Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:20:49 -0500 Subject: [PATCH 70/76] Refactor to allow ruff to identify bare excepts Replaced `except Exception` and `except BaseException` with just `except`. All are functionally equivalent, but ruff sadly only picks up on bare `except`, so this change was necessary to allow these occurrences to be identified by ruff in the future. Unfortunately, `except Exception as e` is still not picked up by ruff. --- tools/RAiDER/cli/statsPlot.py | 6 +++--- tools/RAiDER/getStationDelays.py | 2 +- tools/RAiDER/gnss/processDelayFiles.py | 2 +- tools/RAiDER/losreader.py | 6 +++--- tools/RAiDER/models/ecmwf.py | 2 +- tools/RAiDER/models/gmao.py | 2 +- tools/RAiDER/models/ncmr.py | 2 +- tools/RAiDER/models/weatherModel.py | 2 +- tools/RAiDER/processWM.py | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tools/RAiDER/cli/statsPlot.py b/tools/RAiDER/cli/statsPlot.py index 73bc3d929..c647b7fbf 100755 --- a/tools/RAiDER/cli/statsPlot.py +++ b/tools/RAiDER/cli/statsPlot.py @@ -647,7 +647,7 @@ def _binned_vario(self, hEff, rawVario, xBin=None): warnings.filterwarnings('ignore', message='Mean of empty slice') hExp.append(np.nanmean(hEff[iBinMask])) expVario.append(np.nanmean(rawVario[iBinMask])) - except BaseException: # TODO: Which error(s)? + except: # TODO: Which error(s)? pass if False in ~np.isnan(hExp): @@ -1391,7 +1391,7 @@ def _reader(self): data = pd.read_csv(self.fname, parse_dates=['Datetime']) data['Date'] = data['Datetime'].apply(lambda x: x.date()) data['Date'] = data['Date'].apply(lambda x: dt.datetime.strptime(x.strftime('%Y-%m-%d'), '%Y-%m-%d')) - except BaseException: + except: data = pd.read_csv(self.fname, parse_dates=['Date']) # check if user-specified key is valid @@ -1511,7 +1511,7 @@ def create_DF(self) -> None: if self.bbox is not None: try: self.bbox = [float(val) for val in self.bbox.split()] - except BaseException: + except: raise Exception( 'Cannot understand the --bounding_box argument. String input is incorrect or path does not exist.' ) diff --git a/tools/RAiDER/getStationDelays.py b/tools/RAiDER/getStationDelays.py index c7b4ec040..13f5e8677 100644 --- a/tools/RAiDER/getStationDelays.py +++ b/tools/RAiDER/getStationDelays.py @@ -100,7 +100,7 @@ def get_delays_UNR(stationFile, filename, dateList, returnTime=None) -> None: trotot, trototSD, trwet, tgetot, tgetotSD, tgntot, tgntotSD, wvapor, wvaporSD, mtemp = ( float(t) for t in split_lines[2:] ) - except BaseException: # TODO: What error(s)? + except: # TODO: What error(s)? continue site = split_lines[0] year, doy, seconds = (int(n) for n in split_lines[1].split(':')) diff --git a/tools/RAiDER/gnss/processDelayFiles.py b/tools/RAiDER/gnss/processDelayFiles.py index dca2ef4b9..21d01a375 100644 --- a/tools/RAiDER/gnss/processDelayFiles.py +++ b/tools/RAiDER/gnss/processDelayFiles.py @@ -38,8 +38,8 @@ def combineDelayFiles(outName, loc=os.getcwd(), source='model', ext='.csv', ref= print(f'Combining {source} delay files') try: concatDelayFiles(files, sort_list=['ID', 'Datetime'], outName=outName, source=source) - except BaseException: concatDelayFiles(files, sort_list=['ID', 'Date'], outName=outName, source=source, ref=ref, col_name=col_name) + except: def addDateTimeToFiles(fileList, force=False, verbose=False) -> None: diff --git a/tools/RAiDER/losreader.py b/tools/RAiDER/losreader.py index 60b1216c0..096e4e867 100644 --- a/tools/RAiDER/losreader.py +++ b/tools/RAiDER/losreader.py @@ -250,7 +250,7 @@ def getLookVectors(self, ht, llh, xyz, yy): ) sat_xyz, _ = self._orbit.interpolate(aztime) los[ii, jj, :] = (sat_xyz - inp_xyz) / slant_range - except Exception: + except: los[ii, jj, :] = np.nan return los @@ -356,10 +356,10 @@ def filter_ESA_orbit_file_p(path: str) -> bool: for orb_path in los_files: svs.extend(read_ESA_Orbit_file(orb_path)) - except BaseException: + except: try: svs = read_shelve(los_file) - except BaseException: + except: raise ValueError(f'get_sv: I cannot parse the statevector file {los_file}') except: raise ValueError(f'get_sv: I cannot parse the statevector file {los_file}') diff --git a/tools/RAiDER/models/ecmwf.py b/tools/RAiDER/models/ecmwf.py index d53d168de..116f6d311 100755 --- a/tools/RAiDER/models/ecmwf.py +++ b/tools/RAiDER/models/ecmwf.py @@ -198,7 +198,7 @@ def _get_from_cds(self, lat_min, lat_max, lon_min, lon_max, acqTime, outname) -> try: c.retrieve('reanalysis-era5-complete', dataDict, outname) - except Exception: + except: raise Exception def _download_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step, time, out) -> None: diff --git a/tools/RAiDER/models/gmao.py b/tools/RAiDER/models/gmao.py index f7130ba40..666fa08f3 100755 --- a/tools/RAiDER/models/gmao.py +++ b/tools/RAiDER/models/gmao.py @@ -137,7 +137,7 @@ def _fetch(self, out) -> None: try: # Note that lat/lon gets written twice for GMAO because they are the same as y/x writeWeatherVarsXarray(lats, lons, h, q, p, t, dt, crs, outName=None, NoDataValue=None, chunk=(1, 91, 144)) - except Exception: + except: logger.exception('Unable to save weathermodel to file') def load_weather(self, f=None) -> None: diff --git a/tools/RAiDER/models/ncmr.py b/tools/RAiDER/models/ncmr.py index b490f68f6..03c0e2c12 100755 --- a/tools/RAiDER/models/ncmr.py +++ b/tools/RAiDER/models/ncmr.py @@ -195,7 +195,7 @@ def _download_ncmr_file(self, out, date_time, bounding_box) -> None: try: writeWeatherVarsXarray(lats, lons, hgt, q, p, t, self._time, self._proj, outName=out) - except Exception: + except: logger.exception('Unable to save weathermodel to file') def _makeDataCubes(self, filename) -> None: diff --git a/tools/RAiDER/models/weatherModel.py b/tools/RAiDER/models/weatherModel.py index 359dc6a27..9bdfc7c7b 100755 --- a/tools/RAiDER/models/weatherModel.py +++ b/tools/RAiDER/models/weatherModel.py @@ -585,7 +585,7 @@ def _uniform_in_z(self, _zlevels=None) -> None: if _zlevels is None: try: _zlevels = self._zlevels - except BaseException: + except: _zlevels = np.nanmean(self._zs, axis=(0, 1)) new_zs = np.tile(_zlevels, (nx, ny, 1)) diff --git a/tools/RAiDER/processWM.py b/tools/RAiDER/processWM.py index f9cc75fdd..9278c7e8d 100755 --- a/tools/RAiDER/processWM.py +++ b/tools/RAiDER/processWM.py @@ -163,5 +163,5 @@ def _weather_model_debug(los, lats, lons, ll_bounds, weather_model, wmLoc, time, ) try: weather_model.write2NETCDF4(weather_model_file) - except Exception: + except: logger.exception('Unable to save weathermodel to file') From 173571de20e5952a1cbd5f39040ed8b74d14c2fd Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:07:44 -0500 Subject: [PATCH 71/76] Add ruff guide to CONTRIBUTING.md --- CONTRIBUTING.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb2173730..7a33f344b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,6 +111,22 @@ git commit -a -m "Put here the synthetic commit message" git push my_user_name my_new_feature_branch ``` +### Formatting and linting with [Ruff](https://docs.astral.sh/ruff/) ### + +Format your code to follow the style of the project with: +``` +ruff format +``` +and check for linting problems with: +``` +ruff check +``` +Please ensure that any linting problems in your changes are resolved before +submitting a pull request. +> [!TIP] +> vscode users can [install the ruff extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) to run the linter automatically in the +editor. + ### Issue a pull request from GitHub UI ### commit locally and push. To get a reasonable history, you may need to From ec9c8cbc377cd6ea5c536028ff7fcaafdae976cf Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 7 Aug 2024 12:22:54 -0500 Subject: [PATCH 72/76] Normalize import aliases The following packages are now imported the same across all files: - import datetime as dt - import xarray as xr - import multiprocessing as mp --- tools/RAiDER/aria/calcGUNW.py | 10 ++-- tools/RAiDER/aria/prepFromGUNW.py | 30 +++++----- tools/RAiDER/checkArgs.py | 2 +- tools/RAiDER/cli/raider.py | 8 +-- tools/RAiDER/cli/statsPlot.py | 6 +- tools/RAiDER/delay.py | 32 ++++++----- tools/RAiDER/getStationDelays.py | 4 +- tools/RAiDER/gnss/downloadGNSSDelays.py | 4 +- tools/RAiDER/gnss/processDelayFiles.py | 27 +++++---- tools/RAiDER/llreader.py | 8 +-- tools/RAiDER/losreader.py | 34 ++++++------ tools/RAiDER/models/ecmwf.py | 18 +++--- tools/RAiDER/models/era5.py | 8 +-- tools/RAiDER/models/era5t.py | 8 +-- tools/RAiDER/models/erai.py | 8 +-- tools/RAiDER/models/gmao.py | 12 ++-- tools/RAiDER/models/hres.py | 12 ++-- tools/RAiDER/models/hrrr.py | 26 ++++----- tools/RAiDER/models/merra2.py | 20 +++---- tools/RAiDER/models/ncmr.py | 8 +-- tools/RAiDER/models/template.py | 8 +-- tools/RAiDER/models/weatherModel.py | 34 ++++++------ tools/RAiDER/s1_azimuth_timing.py | 74 ++++++++++++------------- tools/RAiDER/s1_orbits.py | 15 ++--- tools/RAiDER/utilFcns.py | 47 ++++++++-------- 25 files changed, 232 insertions(+), 231 deletions(-) diff --git a/tools/RAiDER/aria/calcGUNW.py b/tools/RAiDER/aria/calcGUNW.py index abf0748fc..2f6af058d 100644 --- a/tools/RAiDER/aria/calcGUNW.py +++ b/tools/RAiDER/aria/calcGUNW.py @@ -2,8 +2,8 @@ Calculate the interferometric phase from the 4 delays files of a GUNW and write it to disk. """ +import datetime as dt import os -from datetime import datetime from pathlib import Path import h5py @@ -41,9 +41,9 @@ def compute_delays_slc(cube_paths: list[Path], wavelength: float) -> xr.Dataset: Formatted dataset for GUNW """ # parse date from filename - dct_delays: dict[datetime, Path] = {} + dct_delays: dict[dt.datetime, Path] = {} for path in cube_paths: - date = datetime.strptime(path.name.split('_')[2], '%Y%m%dT%H%M%S') + date = dt.datetime.strptime(path.name.split('_')[2], '%Y%m%dT%H%M%S') dct_delays[date] = path sec, ref = sorted(dct_delays.keys()) @@ -52,8 +52,8 @@ def compute_delays_slc(cube_paths: list[Path], wavelength: float) -> xr.Dataset: hyd_delays: list[xr.DataArray] = [] attrs_lst: list[dict] = [] phase2range = (-4 * np.pi) / float(wavelength) - for dt in [ref, sec]: - path = dct_delays[dt] + for datetime in [ref, sec]: + path = dct_delays[datetime] with xr.open_dataset(path) as ds: da_wet = ds['wet'] * phase2range da_hydro = ds['hydro'] * phase2range diff --git a/tools/RAiDER/aria/prepFromGUNW.py b/tools/RAiDER/aria/prepFromGUNW.py index 65a9c27f4..a6a6a5a60 100644 --- a/tools/RAiDER/aria/prepFromGUNW.py +++ b/tools/RAiDER/aria/prepFromGUNW.py @@ -5,8 +5,8 @@ # RESERVED. United States Government Sponsorship acknowledged. # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +import datetime as dt import sys -from datetime import datetime, timedelta, timezone from pathlib import Path import numpy as np @@ -29,7 +29,7 @@ DCT_POSTING = {'HRRR': 0.05, 'HRES': 0.10, 'GMAO': 0.10, 'ERA5': 0.10, 'ERA5T': 0.10, 'MERRA2': 0.1} -def _get_acq_time_from_gunw_id(gunw_id: str, reference_or_secondary: str) -> datetime: +def _get_acq_time_from_gunw_id(gunw_id: str, reference_or_secondary: str) -> dt.datetime: # Ex: S1-GUNW-A-R-106-tops-20220115_20211222-225947-00078W_00041N-PP-4be8-v3_0_0 if reference_or_secondary not in ['reference', 'secondary']: raise ValueError('Reference_or_secondary must "reference" or "secondary"') @@ -37,7 +37,7 @@ def _get_acq_time_from_gunw_id(gunw_id: str, reference_or_secondary: str) -> dat date_tokens = tokens[6].split('_') date_token = date_tokens[0] if reference_or_secondary == 'reference' else date_tokens[1] center_time_token = tokens[7] - cen_acq_time = datetime( + cen_acq_time = dt.datetime( int(date_token[:4]), int(date_token[4:6]), int(date_token[6:]), @@ -120,8 +120,8 @@ def check_weather_model_availability(gunw_path: Path, weather_model_name: str) - ref_slc_ids = get_slc_ids_from_gunw(gunw_path, reference_or_secondary='reference') sec_slc_ids = get_slc_ids_from_gunw(gunw_path, reference_or_secondary='secondary') - ref_ts = get_acq_time_from_slc_id(ref_slc_ids[0]).replace(tzinfo=timezone(offset=timedelta())) - sec_ts = get_acq_time_from_slc_id(sec_slc_ids[0]).replace(tzinfo=timezone(offset=timedelta())) + ref_ts = get_acq_time_from_slc_id(ref_slc_ids[0]).replace(tzinfo=dt.timezone(offset=dt.timedelta())) + sec_ts = get_acq_time_from_slc_id(sec_slc_ids[0]).replace(tzinfo=dt.timezone(offset=dt.timedelta())) if weather_model_name == 'HRRR': group = '/science/grids/data/' @@ -147,7 +147,7 @@ def check_weather_model_availability(gunw_path: Path, weather_model_name: str) - weather_model = weather_model_cls() wm_start_date, wm_end_date = weather_model._valid_range - if not isinstance(wm_end_date, datetime): + if not isinstance(wm_end_date, dt.datetime): raise ValueError(f"the weather model's end date is not valid: {wm_end_date}") ref_cond = ref_ts <= wm_end_date sec_cond = sec_ts >= wm_start_date @@ -211,10 +211,10 @@ def get_datetimes(self) -> tuple[list[int], str]: mid_time = midpoint.time().strftime('%H:%M:%S') return mid_dates, mid_time - def get_slc_dt(self) -> list[tuple[datetime, datetime]]: + def get_slc_dt(self) -> list[tuple[dt.datetime, dt.datetime]]: """Grab the SLC start date and time from the GUNW.""" group = 'science/radarMetaData/inputSLC' - lst_sten: list[tuple[datetime, datetime]] = [] + lst_sten: list[tuple[dt.datetime, dt.datetime]] = [] for key in 'reference secondary'.split(): with xr.open_dataset(self.path_gunw, group=f'{group}/{key}') as ds: slcs = ds['L1InputGranules'] @@ -223,19 +223,19 @@ def get_slc_dt(self) -> list[tuple[datetime, datetime]]: if nslcs == 1: slc = slcs.item() assert slc, f'Missing {key} SLC metadata in GUNW: {self.f}' - st = datetime.strptime(slc.split('_')[5], '%Y%m%dT%H%M%S') - en = datetime.strptime(slc.split('_')[6], '%Y%m%dT%H%M%S') + st = dt.datetime.strptime(slc.split('_')[5], '%Y%m%dT%H%M%S') + en = dt.datetime.strptime(slc.split('_')[6], '%Y%m%dT%H%M%S') else: - st, en = datetime(1989, 3, 1), datetime(1989, 3, 1) + st, en = dt.datetime(1989, 3, 1), dt.datetime(1989, 3, 1) for j in range(nslcs): slc = slcs.data[j] if slc: # get the maximum range - st_tmp = datetime.strptime(slc.split('_')[5], '%Y%m%dT%H%M%S') - en_tmp = datetime.strptime(slc.split('_')[6], '%Y%m%dT%H%M%S') + st_tmp = dt.datetime.strptime(slc.split('_')[5], '%Y%m%dT%H%M%S') + en_tmp = dt.datetime.strptime(slc.split('_')[6], '%Y%m%dT%H%M%S') # check the second SLC is within one day of the previous - if st > datetime(1989, 3, 1): + if st > dt.datetime(1989, 3, 1): stdiff = np.abs((st_tmp - st).days) endiff = np.abs((en_tmp - en).days) assert stdiff < 2 and endiff < 2, 'SLCs granules are too far apart in time. Incorrect metadata' @@ -243,7 +243,7 @@ def get_slc_dt(self) -> list[tuple[datetime, datetime]]: st = st_tmp if st_tmp > st else st en = en_tmp if en_tmp > en else en - assert st > datetime(1989, 3, 1), \ + assert st > dt.datetime(1989, 3, 1), \ f'Missing {key} SLC metadata in GUNW: {self.f}' lst_sten.append((st, en)) diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index 5397681f0..0a9b06500 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -115,7 +115,7 @@ def makeDelayFileNames(date: Optional[dt.date], los: Optional[LOS], outformat: s return names for the wet and hydrostatic delays. # Examples: - >>> makeDelayFileNames(datetime(2020, 1, 1, 0, 0, 0), None, "h5", "model_name", "some_dir") + >>> makeDelayFileNames(dt.datetime(2020, 1, 1, 0, 0, 0), None, "h5", "model_name", "some_dir") ('some_dir/model_name_wet_00_00_00_ztd.h5', 'some_dir/model_name_hydro_00_00_00_ztd.h5') >>> makeDelayFileNames(None, None, "h5", "model_name", "some_dir") ('some_dir/model_name_wet_ztd.h5', 'some_dir/model_name_hydro_ztd.h5') diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 029f0ac78..2cb9c39a4 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -1,5 +1,5 @@ import argparse -import datetime +import datetime as dt import json import os import shutil @@ -263,7 +263,7 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: model.set_latlon_bounds(wm_bounds, output_spacing=aoi.get_output_spacing()) wet_paths: list[Path] = [] - t: datetime.datetime + t: dt.datetime w: str f: str for t, w, f in zip(run_config.date_group.date_list, run_config.wetFilenames, run_config.hydroFilenames): @@ -773,7 +773,7 @@ def combine_weather_files(wfiles, t, model, interp_method='center_time'): # Pull the datetimes from the datasets times = [] for ds in datasets: - times.append(datetime.datetime.strptime(ds.attrs['datetime'], '%Y_%m_%dT%H_%M_%S')) + times.append(dt.datetime.strptime(ds.attrs['datetime'], '%Y_%m_%dT%H_%M_%S')) if len(times) == 0: raise NoWeatherModelData() @@ -816,7 +816,7 @@ def combine_files_using_azimuth_time(wfiles, t, times): # Pull the datetimes from the datasets times = [] for ds in datasets: - times.append(datetime.datetime.strptime(ds.attrs['datetime'], '%Y_%m_%dT%H_%M_%S')) + times.append(dt.datetime.strptime(ds.attrs['datetime'], '%Y_%m_%dT%H_%M_%S')) model = datasets[0].attrs['model_name'] diff --git a/tools/RAiDER/cli/statsPlot.py b/tools/RAiDER/cli/statsPlot.py index c647b7fbf..bddde5cb9 100755 --- a/tools/RAiDER/cli/statsPlot.py +++ b/tools/RAiDER/cli/statsPlot.py @@ -9,7 +9,7 @@ import copy import datetime as dt import itertools -import multiprocessing +import multiprocessing as mp import os import warnings @@ -861,7 +861,7 @@ def create_variograms(self): grid_subset = self.df[self.df['gridnode'] == i] args.append((i, grid_subset)) # Parallelize iteration through all grid-cells and time slices - with multiprocessing.Pool(self.numCPUs) as multipool: + with mp.Pool(self.numCPUs) as multipool: for i, j, k, l in multipool.starmap(self._append_variogram, args): self.TOT_good_slices.extend(i) self.TOT_res_robust_arr.extend(j) @@ -1819,7 +1819,7 @@ def create_DF(self) -> None: ) ) # Parallelize iteration through all grid-cells and time slices - with multiprocessing.Pool(self.numCPUs) as multipool: + with mp.Pool(self.numCPUs) as multipool: for i, j, k, l, m, n, o in multipool.starmap(self._amplitude_and_phase, args): self.ampfit.extend(i) self.phsfit.extend(j) diff --git a/tools/RAiDER/delay.py b/tools/RAiDER/delay.py index 47408982e..24a9538b9 100755 --- a/tools/RAiDER/delay.py +++ b/tools/RAiDER/delay.py @@ -13,15 +13,15 @@ "wet_total" and "hydro_total" fields specified. """ +import datetime as dt import os -from datetime import datetime, timezone from typing import Optional, Union from RAiDER.types import CRSLike from RAiDER.utilFcns import parse_crs import numpy as np import pyproj -import xarray +import xarray as xr from pyproj import CRS, Transformer from RAiDER.constants import _ZREF @@ -29,11 +29,13 @@ from RAiDER.llreader import AOI, BoundingBox, Geocube from RAiDER.logger import logger from RAiDER.losreader import LOS, build_ray +from RAiDER.types import CRSLike +from RAiDER.utilFcns import parse_crs ############################################################################### def tropo_delay( - dt: datetime, + datetime: dt.datetime, weather_model_file: str, aoi: AOI, los: LOS, @@ -49,7 +51,7 @@ def tropo_delay( 3. Slant delays integrated along the raypath (STD-raytracing) Args: - dt: Datetime - Datetime object for determining when to calculate delays + datetime: Datetime - Datetime object for determining when to calculate delays weather_model_File: string - Name of the NETCDF file containing a pre-processed weather model aoi: AOI object - AOI object los: LOS object - LOS object @@ -63,7 +65,7 @@ def tropo_delay( crs = CRS(out_proj) # Load CRS from weather model file - with xarray.load_dataset(weather_model_file) as ds: + with xr.load_dataset(weather_model_file) as ds: try: wm_proj = CRS.from_wkt(ds['proj'].attrs['crs_wkt']) except KeyError: @@ -73,7 +75,7 @@ def tropo_delay( wm_proj = CRS.from_epsg(4326) # get heights - with xarray.load_dataset(weather_model_file) as ds: + with xr.load_dataset(weather_model_file) as ds: wm_levels = ds.z.values toa = wm_levels.max() - 1 @@ -93,7 +95,7 @@ def tropo_delay( ) # TODO: expose this as library function - ds = _get_delays_on_cube(dt, weather_model_file, wm_proj, aoi, height_levels, los, crs, zref) + ds = _get_delays_on_cube(datetime, weather_model_file, wm_proj, aoi, height_levels, los, crs, zref) if isinstance(aoi, (BoundingBox, Geocube)): return ds, None @@ -120,7 +122,7 @@ def tropo_delay( # return the delays (ZTD or STD) if los.is_Projected(): - los.setTime(dt) + los.setTime(datetime) los.setPoints(lats, lons, hgts) wetDelay = los(wetDelay) hydroDelay = los(hydroDelay) @@ -128,14 +130,14 @@ def tropo_delay( return wetDelay, hydroDelay -def _get_delays_on_cube(dt, weather_model_file, wm_proj, aoi, heights, los, crs, zref, nproc=1): +def _get_delays_on_cube(datetime: dt.datetime, weather_model_file, wm_proj, aoi, heights, los, crs, zref, nproc=1): """Raider cube generation function.""" zpts = np.array(heights) try: aoi.xpts except AttributeError: - with xarray.load_dataset(weather_model_file) as ds: + with xr.load_dataset(weather_model_file) as ds: x_spacing = ds.x.diff(dim='x').values.mean() y_spacing = ds.y.diff(dim='y').values.mean() aoi.set_output_spacing(ll_res=np.min([x_spacing, y_spacing])) @@ -186,7 +188,7 @@ def _get_delays_on_cube(dt, weather_model_file, wm_proj, aoi, heights, los, crs, logger.critical('There are missing delay values. Check your inputs.') # Write output file - ds = writeResultsToXarray(dt, aoi.xpts, aoi.ypts, zpts, crs, wetDelay, hydroDelay, weather_model_file, out_type) + ds = writeResultsToXarray(datetime, aoi.xpts, aoi.ypts, zpts, crs, wetDelay, hydroDelay, weather_model_file, out_type) return ds @@ -324,10 +326,10 @@ def _build_cube_ray( return outputArrs -def writeResultsToXarray(dt, xpts, ypts, zpts, crs, wetDelay, hydroDelay, weather_model_file, out_type): +def writeResultsToXarray(datetime: dt.datetime, xpts, ypts, zpts, crs, wetDelay, hydroDelay, weather_model_file, out_type): """Write a 1-D array to a NETCDF5 file.""" # Modify this as needed for NISAR / other projects - ds = xarray.Dataset( + ds = xr.Dataset( data_vars=dict( wet=( ['z', 'y', 'x'], @@ -359,9 +361,9 @@ def writeResultsToXarray(dt, xpts, ypts, zpts, crs, wetDelay, hydroDelay, weathe Conventions='CF-1.7', title='RAiDER geo cube', source=os.path.basename(weather_model_file), - history=str(datetime.now(tz=timezone.utc)) + ' RAiDER', + history=str(dt.datetime.now(tz=dt.timezone.utc)) + ' RAiDER', description=f'RAiDER geo cube - {out_type}', - reference_time=dt.strftime('%Y%m%dT%H:%M:%S'), + reference_time=datetime.strftime('%Y%m%dT%H:%M:%S'), ), ) diff --git a/tools/RAiDER/getStationDelays.py b/tools/RAiDER/getStationDelays.py index 13f5e8677..9e77d07ee 100644 --- a/tools/RAiDER/getStationDelays.py +++ b/tools/RAiDER/getStationDelays.py @@ -8,7 +8,7 @@ import datetime as dt import gzip import io -import multiprocessing +import multiprocessing as mp import os import zipfile @@ -224,7 +224,7 @@ def get_station_data(inFile, dateList, gps_repo=None, numCPUs=8, outDir=None, re args.append((sf, name, dateList, returnTime)) outputfiles.append(name) # Parallelize remote querying of zenith delays - with multiprocessing.Pool(numCPUs) as multipool: + with mp.Pool(numCPUs) as multipool: multipool.starmap(get_delays_UNR, args) # confirm file exists (i.e. valid delays exists for specified time/region). diff --git a/tools/RAiDER/gnss/downloadGNSSDelays.py b/tools/RAiDER/gnss/downloadGNSSDelays.py index db634ccbb..67c97c9f6 100755 --- a/tools/RAiDER/gnss/downloadGNSSDelays.py +++ b/tools/RAiDER/gnss/downloadGNSSDelays.py @@ -6,7 +6,7 @@ # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import itertools -import multiprocessing +import multiprocessing as mp import os import pandas as pd @@ -126,7 +126,7 @@ def download_tropo_delays( # Parallelize remote querying of station locations results = [] - with multiprocessing.Pool(numCPUs) as multipool: + with mp.Pool(numCPUs) as multipool: # only record valid path if gps_repo == 'UNR': results = [fileurl for fileurl in multipool.starmap(download_UNR, stat_year_tup) if fileurl['path']] diff --git a/tools/RAiDER/gnss/processDelayFiles.py b/tools/RAiDER/gnss/processDelayFiles.py index 21d01a375..24eaa4766 100644 --- a/tools/RAiDER/gnss/processDelayFiles.py +++ b/tools/RAiDER/gnss/processDelayFiles.py @@ -1,6 +1,6 @@ import argparse -import datetime import glob +import datetime as dt import math import os import re @@ -58,8 +58,7 @@ def addDateTimeToFiles(fileList, force=False, verbose=False) -> None: ) else: try: - dt = getDateTime(f) - data['Datetime'] = dt + data['Datetime'] = getDateTime(path) # drop all lines with nans data.dropna(how='any', inplace=True) # drop all duplicate lines @@ -82,29 +81,29 @@ def update_time(row, localTime_hrs): """Update with local origin time.""" localTime_estimate = row['Datetime'].replace(hour=localTime_hrs, minute=0, second=0) # determine if you need to shift days - time_shift = datetime.timedelta(days=0) + time_shift = dt.timedelta(days=0) # round to nearest hour days_diff = ( - row['Datetime'] - datetime.timedelta(seconds=math.floor(row['Localtime']) * 3600) + row['Datetime'] - dt.timedelta(seconds=math.floor(row['Localtime']) * 3600) ).day - localTime_estimate.day # if lon <0, check if you need to add day if row['Lon'] < 0: # add day if days_diff != 0: - time_shift = datetime.timedelta(days=1) + time_shift = dt.timedelta(days=1) # if lon >0, check if you need to subtract day if row['Lon'] > 0: # subtract day if days_diff != 0: - time_shift = -datetime.timedelta(days=1) - return localTime_estimate + datetime.timedelta(seconds=row['Localtime'] * 3600) + time_shift + time_shift = -dt.timedelta(days=1) + return localTime_estimate + dt.timedelta(seconds=row['Localtime'] * 3600) + time_shift def pass_common_obs(reference, target, localtime=None): """Pass only observations in target spatiotemporally common to reference.""" if isinstance(target['Datetime'].iloc[0], str): target['Datetime'] = target['Datetime'].apply( - lambda x: datetime.datetime.strptime(x, '%Y-%m-%d %H:%M:%S') + lambda x: dt.datetime.strptime(x, '%Y-%m-%d %H:%M:%S') ) if localtime: return target[ @@ -172,8 +171,8 @@ def local_time_filter(raiderFile, ztdFile, dfr, dfz, localTime): dfz['Localtime'] = dfz.apply(lambda r: update_time(r, localTime_hrs), axis=1) # filter out data outside of --localtime hour threshold - dfr['Localtime_u'] = dfr['Localtime'] + datetime.timedelta(hours=localTime_hrthreshold) - dfr['Localtime_l'] = dfr['Localtime'] - datetime.timedelta(hours=localTime_hrthreshold) + dfr['Localtime_u'] = dfr['Localtime'] + dt.timedelta(hours=localTime_hrthreshold) + dfr['Localtime_l'] = dfr['Localtime'] - dt.timedelta(hours=localTime_hrthreshold) OG_total = dfr.shape[0] dfr = dfr[(dfr['Datetime'] >= dfr['Localtime_l']) & (dfr['Datetime'] <= dfr['Localtime_u'])] @@ -182,8 +181,8 @@ def local_time_filter(raiderFile, ztdFile, dfr, dfz, localTime): f'Total number of datapoints dropped in {raiderFile} for not being within {localTime.split(" ")[1]} hrs of ' f'specified local-time {localTime.split(" ")[0]}: {dfr.shape[0]} out of {OG_total}' ) - dfz['Localtime_u'] = dfz['Localtime'] + datetime.timedelta(hours=localTime_hrthreshold) - dfz['Localtime_l'] = dfz['Localtime'] - datetime.timedelta(hours=localTime_hrthreshold) + dfz['Localtime_u'] = dfz['Localtime'] + dt.timedelta(hours=localTime_hrthreshold) + dfz['Localtime_l'] = dfz['Localtime'] - dt.timedelta(hours=localTime_hrthreshold) OG_total = dfz.shape[0] dfz = dfz[(dfz['Datetime'] >= dfz['Localtime_l']) & (dfz['Datetime'] <= dfz['Localtime_u'])] # only keep observation closest to Localtime @@ -209,7 +208,7 @@ def readZTDFile(filename, col_name='ZTD'): """Read and parse a GPS zenith delay file.""" try: data = pd.read_csv(filename, parse_dates=['Date']) - times = data['times'].apply(lambda x: datetime.timedelta(seconds=x)) + times = data['times'].apply(lambda x: dt.timedelta(seconds=x)) data['Datetime'] = data['Date'] + times except (KeyError, ValueError): data = pd.read_csv(filename, parse_dates=['Datetime']) diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index 7056ad291..ca765f107 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -13,7 +13,7 @@ from RAiDER.types import BB, RIO import numpy as np import pyproj -import xarray +import xarray as xr try: @@ -369,21 +369,21 @@ def __init__(self, path_cube, cube_spacing_in_m: Optional[float]=None) -> None: _, self._proj, self._geotransform = rio_stats(path_cube) def get_extent(self): - with xarray.open_dataset(self.path) as ds: + with xr.open_dataset(self.path) as ds: S, N = ds.latitude.min().item(), ds.latitude.max().item() W, E = ds.longitude.min().item(), ds.longitude.max().item() return [S, N, W, E] ## untested def readLL(self) -> tuple[np.ndarray, np.ndarray]: - with xarray.open_dataset(self.path) as ds: + with xr.open_dataset(self.path) as ds: lats = ds.latitutde.data() lons = ds.longitude.data() Lats, Lons = np.meshgrid(lats, lons) return Lats, Lons def readZ(self): - with xarray.open_dataset(self.path) as ds: + with xr.open_dataset(self.path) as ds: heights = ds.heights.data return heights diff --git a/tools/RAiDER/losreader.py b/tools/RAiDER/losreader.py index 096e4e867..7d5e44f13 100644 --- a/tools/RAiDER/losreader.py +++ b/tools/RAiDER/losreader.py @@ -6,7 +6,7 @@ # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -import datetime +import datetime as dt import os import shelve from abc import ABC @@ -59,8 +59,8 @@ def setPoints(self, lats, lons=None, heights=None) -> None: self._lons = lons self._heights = heights - def setTime(self, dt) -> None: - self._time = dt + def setTime(self, datetime) -> None: + self._time = datetime def is_Zenith(self): return self._is_zenith @@ -316,7 +316,7 @@ def getZenithLookVecs(lats, lons, heights): return np.stack([x, y, z], axis=-1) -def get_sv(los_file: Union[str, list, PosixPath], ref_time: datetime.datetime, pad: int): +def get_sv(los_file: Union[str, list, PosixPath], ref_time: dt.datetime, pad: int): """ Read an LOS file and return orbital state vectors. @@ -453,7 +453,7 @@ def read_txt_file(filename): for line in f: try: parts = line.strip().split() - t_ = datetime.datetime.fromisoformat(parts[0]) + t_ = dt.datetime.fromisoformat(parts[0]) x_, y_, z_, vx_, vy_, vz_ = (float(t) for t in parts[1:]) except ValueError: raise ValueError( @@ -506,7 +506,7 @@ def read_ESA_Orbit_file(filename): vz = np.ones(numOSV) for i, st in enumerate(data_block[0]): - t.append(datetime.datetime.strptime(st[1].text, 'UTC=%Y-%m-%dT%H:%M:%S.%f')) + t.append(dt.datetime.strptime(st[1].text, 'UTC=%Y-%m-%dT%H:%M:%S.%f')) x[i] = float(st[4].text) y[i] = float(st[5].text) @@ -518,13 +518,13 @@ def read_ESA_Orbit_file(filename): return [t, x, y, z, vx, vy, vz] -def pick_ESA_orbit_file(list_files: list, ref_time: datetime.datetime): +def pick_ESA_orbit_file(list_files: list, ref_time: dt.datetime): """From list of .EOF orbit files, pick the one that contains 'ref_time'.""" orb_file = None for path in list_files: f = os.path.basename(path) - t0 = datetime.datetime.strptime(f.split('_')[6].lstrip('V'), '%Y%m%dT%H%M%S') - t1 = datetime.datetime.strptime(f.split('_')[7].rstrip('.EOF'), '%Y%m%dT%H%M%S') + t0 = dt.datetime.strptime(f.split('_')[6].lstrip('V'), '%Y%m%dT%H%M%S') + t1 = dt.datetime.strptime(f.split('_')[7].rstrip('.EOF'), '%Y%m%dT%H%M%S') if t0 < ref_time < t1: orb_file = path break @@ -534,14 +534,14 @@ def pick_ESA_orbit_file(list_files: list, ref_time: datetime.datetime): return path -def filter_ESA_orbit_file(orbit_xml: str, ref_time: datetime.datetime) -> bool: +def filter_ESA_orbit_file(orbit_xml: str, ref_time: dt.datetime) -> bool: """Returns true or false depending on whether orbit file contains ref time. Parameters ---------- orbit_xml : str ESA orbit xml - ref_time : datetime.datetime + ref_time : dt.datetime Returns: ------- @@ -549,8 +549,8 @@ def filter_ESA_orbit_file(orbit_xml: str, ref_time: datetime.datetime) -> bool: True if ref time is within orbit_xml """ f = os.path.basename(orbit_xml) - t0 = datetime.datetime.strptime(f.split('_')[6].lstrip('V'), '%Y%m%dT%H%M%S') - t1 = datetime.datetime.strptime(f.split('_')[7].rstrip('.EOF'), '%Y%m%dT%H%M%S') + t0 = dt.datetime.strptime(f.split('_')[6].lstrip('V'), '%Y%m%dT%H%M%S') + t1 = dt.datetime.strptime(f.split('_')[7].rstrip('.EOF'), '%Y%m%dT%H%M%S') return t0 < ref_time < t1 @@ -571,12 +571,12 @@ def state_to_los(svs, llh_targets): LOS - * x 3 matrix of LOS unit vectors in ECEF (*not* ENU) Example: - >>> import datetime - >>> import numpy + >>> import datetime as dt + >>> import numpy as np >>> from RAiDER.utilFcns import rio_open >>> import RAiDER.losreader as losr >>> lats, lons, heights = np.array([-76.1]), np.array([36.83]), np.array([0]) - >>> time = datetime.datetime(2018,11,12,23,0,0) + >>> time = dt.datetime(2018,11,12,23,0,0) >>> # download the orbit file beforehand >>> esa_orbit_file = 'S1A_OPER_AUX_POEORB_OPOD_20181203T120749_V20181112T225942_20181114T005942.EOF' >>> svs = losr.read_ESA_Orbit_file(esa_orbit_file) @@ -733,7 +733,7 @@ def getTopOfAtmosphere(xyz, look_vecs, toaheight, factor=None): return pos -def get_orbit(orbit_file: Union[list, str], ref_time: datetime.datetime, pad: int): +def get_orbit(orbit_file: Union[list, str], ref_time: dt.datetime, pad: int): """ Returns state vectors from an orbit file; state vectors are unique and ordered in terms of time orbit file (str | list): - user-passed file(s) containing statevectors diff --git a/tools/RAiDER/models/ecmwf.py b/tools/RAiDER/models/ecmwf.py index 116f6d311..e046ee994 100755 --- a/tools/RAiDER/models/ecmwf.py +++ b/tools/RAiDER/models/ecmwf.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt import numpy as np import xarray as xr @@ -122,7 +122,7 @@ def _get_from_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step server = ecmwfapi.ECMWFDataServer() - corrected_DT = util.round_date(time, datetime.timedelta(hours=self._time_res)) + corrected_DT = util.round_date(time, dt.timedelta(hours=self._time_res)) if not corrected_DT == time: logger.warning('Rounded given datetime from %s to %s', time, corrected_DT) @@ -138,14 +138,14 @@ def _get_from_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step 'stream': 'oper', # date: Specify a single date as "2015-08-01" or a period as # "2015-08-01/to/2015-08-31". - 'date': datetime.datetime.strftime(corrected_DT, '%Y-%m-%d'), + 'date': dt.datetime.strftime(corrected_DT, '%Y-%m-%d'), # type: Use an (analysis) unless you have a particular reason to # use fc (forecast). 'type': 'an', # time: With type=an, time can be any of # "00:00:00/06:00:00/12:00:00/18:00:00". With type=fc, time can # be any of "00:00:00/12:00:00", - 'time': datetime.time.strftime(corrected_DT.time(), '%H:%M:%S'), + 'time': dt.time.strftime(corrected_DT.time(), '%H:%M:%S'), # step: With type=an, step is always "0". With type=fc, step can # be any of "3/6/9/12". 'step': '0', @@ -173,7 +173,7 @@ def _get_from_cds(self, lat_min, lat_max, lon_min, lon_max, acqTime, outname) -> # round to the closest legal time - corrected_DT = util.round_date(acqTime, datetime.timedelta(hours=self._time_res)) + corrected_DT = util.round_date(acqTime, dt.timedelta(hours=self._time_res)) if not corrected_DT == acqTime: logger.warning('Rounded given datetime from %s to %s', acqTime, corrected_DT) @@ -187,7 +187,7 @@ def _get_from_cds(self, lat_min, lat_max, lon_min, lon_max, acqTime, outname) -> 'stream': 'oper', 'type': 'an', 'date': corrected_DT.strftime('%Y-%m-%d'), - 'time': datetime.time.strftime(corrected_DT.time(), '%H:%M'), + 'time': dt.time.strftime(corrected_DT.time(), '%H:%M'), # step: With type=an, step is always "0". With type=fc, step can # be any of "3/6/9/12". 'step': '0', @@ -208,7 +208,7 @@ def _download_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step server = ECMWFService('mars') # round to the closest legal time - corrected_DT = util.round_date(time, datetime.timedelta(hours=self._time_res)) + corrected_DT = util.round_date(time, dt.timedelta(hours=self._time_res)) if not corrected_DT == time: logger.warning('Rounded given datetime from %s to %s', time, corrected_DT) @@ -228,8 +228,8 @@ def _download_ecmwf(self, lat_min, lat_max, lat_step, lon_min, lon_max, lon_step 'levelist': 'all', 'levtype': f'{self._model_level_type}', 'param': param, - 'date': datetime.datetime.strftime(corrected_DT, '%Y-%m-%d'), - 'time': datetime.time.strftime(corrected_DT.time(), '%H:%M'), + 'date': dt.datetime.strftime(corrected_DT, '%Y-%m-%d'), + 'time': dt.time.strftime(corrected_DT.time(), '%H:%M'), 'step': '0', 'grid': f'{lon_step}/{lat_step}', 'area': f'{lat_max}/{util.floorish(lon_min, 0.1)}/{util.floorish(lat_min, 0.1)}/{lon_max}', diff --git a/tools/RAiDER/models/era5.py b/tools/RAiDER/models/era5.py index 64df87521..c4e8def5d 100755 --- a/tools/RAiDER/models/era5.py +++ b/tools/RAiDER/models/era5.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt from dateutil.relativedelta import relativedelta from pyproj import CRS @@ -21,10 +21,10 @@ def __init__(self) -> None: # Tuple of min/max years where data is available. lag_time = 3 # months - end_date = datetime.datetime.today() - relativedelta(months=lag_time) + end_date = dt.datetime.today() - relativedelta(months=lag_time) self._valid_range = ( - datetime.datetime(1950, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - end_date.replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), + dt.datetime(1950, 1, 1).replace(tzinfo=dt.timezone(offset=dt.timedelta())), + end_date.replace(tzinfo=dt.timezone(offset=dt.timedelta())), ) # Availability lag time in days diff --git a/tools/RAiDER/models/era5t.py b/tools/RAiDER/models/era5t.py index 630bef086..577759a20 100644 --- a/tools/RAiDER/models/era5t.py +++ b/tools/RAiDER/models/era5t.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt from RAiDER.models.era5 import ERA5 @@ -14,10 +14,10 @@ def __init__(self) -> None: self._Name = 'ERA-5T' self._valid_range = ( - datetime.datetime(1950, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc), + dt.datetime(1950, 1, 1).replace(tzinfo=dt.timezone(offset=dt.timedelta())), + dt.datetime.now(dt.timezone.utc), ) # Tuple of min/max years where data is available. # Availability lag time in days; actually about 12 hours but unstable on ECMWF side # https://confluence.ecmwf.int/display/CKB/ERA5%3A+data+documentation # see data update frequency - self._lag_time = datetime.timedelta(days=1) + self._lag_time = dt.timedelta(days=1) diff --git a/tools/RAiDER/models/erai.py b/tools/RAiDER/models/erai.py index d47ebfc09..836ae6676 100755 --- a/tools/RAiDER/models/erai.py +++ b/tools/RAiDER/models/erai.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt from RAiDER.models.ecmwf import ECMWF from RAiDER.models.model_levels import A_ERAI, B_ERAI @@ -17,11 +17,11 @@ def __init__(self) -> None: # Tuple of min/max years where data is available. self._valid_range = ( - datetime.datetime(1979, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime(2019, 8, 31).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), + dt.datetime(1979, 1, 1).replace(tzinfo=dt.timezone(offset=dt.timedelta())), + dt.datetime(2019, 8, 31).replace(tzinfo=dt.timezone(offset=dt.timedelta())), ) - self._lag_time = datetime.timedelta(days=30) # Availability lag time in days + self._lag_time = dt.timedelta(days=30) # Availability lag time in days def __model_levels__(self): self._levels = 60 diff --git a/tools/RAiDER/models/gmao.py b/tools/RAiDER/models/gmao.py index 666fa08f3..830070df6 100755 --- a/tools/RAiDER/models/gmao.py +++ b/tools/RAiDER/models/gmao.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt import os import shutil @@ -31,10 +31,10 @@ def __init__(self) -> None: # Tuple of min/max years where data is available. self._valid_range = ( - datetime.datetime(2014, 2, 20).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc), + dt.datetime(2014, 2, 20).replace(tzinfo=dt.timezone(offset=dt.timedelta())), + dt.datetime.now(dt.timezone.utc), ) - self._lag_time = datetime.timedelta(hours=24.0) # Availability lag time in hours + self._lag_time = dt.timedelta(hours=24.0) # Availability lag time in hours # model constants self._k1 = 0.776 # [K/Pa] @@ -68,9 +68,9 @@ def _fetch(self, out) -> None: lon_min_ind = int((self._ll_bounds[2] - (-180.0)) / self._lon_res) lon_max_ind = int((self._ll_bounds[3] - (-180.0)) / self._lon_res) - T0 = datetime.datetime(2017, 12, 1, 0, 0, 0) + T0 = dt.datetime(2017, 12, 1, 0, 0, 0) # round time to nearest third hour - corrected_DT = round_date(acqTime, datetime.timedelta(hours=self._time_res)) + corrected_DT = round_date(acqTime, dt.timedelta(hours=self._time_res)) if not corrected_DT == acqTime: logger.warning('Rounded given datetime from %s to %s', acqTime, corrected_DT) diff --git a/tools/RAiDER/models/hres.py b/tools/RAiDER/models/hres.py index e065f8ecf..926b09e79 100755 --- a/tools/RAiDER/models/hres.py +++ b/tools/RAiDER/models/hres.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt import numpy as np from pyproj import CRS @@ -41,11 +41,11 @@ def __init__(self, level_type='ml') -> None: self._time_res = TIME_RES[self._dataset.upper()] # Tuple of min/max years where data is available. self._valid_range = ( - datetime.datetime(1983, 4, 20).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc), + dt.datetime(1983, 4, 20).replace(tzinfo=dt.timezone(offset=dt.timedelta())), + dt.datetime.now(dt.timezone.utc), ) # Availability lag time in days - self._lag_time = datetime.timedelta(hours=6) + self._lag_time = dt.timedelta(hours=6) self.setLevelType('ml') @@ -67,7 +67,7 @@ def load_weather(self, f=None) -> None: f = self.files[0] if f is None else f if self._model_level_type == 'ml': - if self._time < datetime.datetime(2013, 6, 26, 0, 0, 0): + if self._time < dt.datetime(2013, 6, 26, 0, 0, 0): self.update_a_b() self._load_model_level(f) elif self._model_level_type == 'pl': @@ -79,7 +79,7 @@ def _fetch(self, out) -> None: lat_min, lat_max, lon_min, lon_max = self._ll_bounds time = self._time - if time < datetime.datetime(2013, 6, 26, 0, 0, 0): + if time < dt.datetime(2013, 6, 26, 0, 0, 0): self.update_a_b() # execute the search at ECMWF diff --git a/tools/RAiDER/models/hrrr.py b/tools/RAiDER/models/hrrr.py index 4fcfa755f..de872d597 100644 --- a/tools/RAiDER/models/hrrr.py +++ b/tools/RAiDER/models/hrrr.py @@ -1,10 +1,10 @@ -import datetime +import datetime as dt import os from pathlib import Path import geopandas as gpd import numpy as np -import xarray +import xarray as xr from herbie import Herbie from pyproj import CRS, Transformer from shapely.geometry import Polygon, box @@ -25,10 +25,10 @@ AK_GEO = gpd.read_file(Path(__file__).parent / 'data' / 'alaska.geojson.zip').geometry.unary_union -def check_hrrr_dataset_availability(dt: datetime) -> bool: +def check_hrrr_dataset_availability(datetime: dt.datetime) -> bool: """Note a file could still be missing within the models valid range.""" herbie = Herbie( - dt, + datetime, model='hrrr', product='nat', fxx=0, @@ -161,7 +161,7 @@ def get_bounds_indices(SNWE, lats, lons): def load_weather_hrrr(filename): """Loads a weather model from a HRRR file.""" # read data from the netcdf file - ds = xarray.open_dataset(filename, engine='netcdf4') + ds = xr.open_dataset(filename, engine='netcdf4') # Pull the relevant data from the file pres = ds['pres'].values.transpose(1, 2, 0) xArr = ds['x'].values @@ -198,10 +198,10 @@ def __init__(self) -> None: # Tuple of min/max years where data is available. self._valid_range = ( - datetime.datetime(2016, 7, 15).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc), + dt.datetime(2016, 7, 15).replace(tzinfo=dt.timezone(offset=dt.timedelta())), + dt.datetime.now(dt.timezone.utc), ) - self._lag_time = datetime.timedelta(hours=3) # Availability lag time in days + self._lag_time = dt.timedelta(hours=3) # Availability lag time in days # model constants self._k1 = 0.776 # [K/Pa] @@ -255,7 +255,7 @@ def __pressure_levels__(self): def _fetch(self, out) -> None: """Fetch weather model data from HRRR.""" self._files = out - corrected_DT = round_date(self._time, datetime.timedelta(hours=self._time_res)) + corrected_DT = round_date(self._time, dt.timedelta(hours=self._time_res)) self.checkTime(corrected_DT) if not corrected_DT == self._time: logger.info('Rounded given datetime from %s to %s', self._time, corrected_DT) @@ -346,10 +346,10 @@ def __init__(self) -> None: self._Name = 'HRRR-AK' self._time_res = TIME_RES['HRRR-AK'] self._valid_range = ( - datetime.datetime(2018, 7, 13).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc), + dt.datetime(2018, 7, 13).replace(tzinfo=dt.timezone(offset=dt.timedelta())), + dt.datetime.now(dt.timezone.utc), ) - self._lag_time = datetime.timedelta(hours=3) + self._lag_time = dt.timedelta(hours=3) self._valid_bounds = HRRR_AK_COVERAGE_POLYGON # The projection information gets read directly from the weather model file but we # keep this here for object instantiation. @@ -368,7 +368,7 @@ def __pressure_levels__(self): def _fetch(self, out) -> None: bounds = self._ll_bounds.copy() bounds[2:] = np.mod(bounds[2:], 360) - corrected_DT = round_date(self._time, datetime.timedelta(hours=self._time_res)) + corrected_DT = round_date(self._time, dt.timedelta(hours=self._time_res)) self.checkTime(corrected_DT) if not corrected_DT == self._time: logger.info(f'Rounded given datetime from {self._time} to {corrected_DT}') diff --git a/tools/RAiDER/models/merra2.py b/tools/RAiDER/models/merra2.py index fc251d42f..82107e558 100755 --- a/tools/RAiDER/models/merra2.py +++ b/tools/RAiDER/models/merra2.py @@ -1,10 +1,10 @@ -import datetime +import datetime as dt import os import numpy as np import pydap.cas.urs import pydap.client -import xarray +import xarray as xr from pyproj import CRS from RAiDER.logger import logger @@ -38,15 +38,15 @@ def __init__(self) -> None: self._dataset = 'merra2' # Tuple of min/max years where data is available. - utcnow = datetime.datetime.now(datetime.timezone.utc) - enddate = datetime.datetime(utcnow.year, utcnow.month, 15) - datetime.timedelta(days=60) - enddate = datetime.datetime(enddate.year, enddate.month, calendar.monthrange(enddate.year, enddate.month)[1]) + utcnow = dt.datetime.now(dt.timezone.utc) + enddate = dt.datetime(utcnow.year, utcnow.month, 15) - dt.timedelta(days=60) + enddate = dt.datetime(enddate.year, enddate.month, calendar.monthrange(enddate.year, enddate.month)[1]) self._valid_range = ( - datetime.datetime(1980, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc), + dt.datetime(1980, 1, 1).replace(tzinfo=dt.timezone(offset=dt.timedelta())), + dt.datetime.now(dt.timezone.utc), ) - lag_time = utcnow - enddate.replace(tzinfo=datetime.timezone(offset=datetime.timedelta())) - self._lag_time = datetime.timedelta(days=lag_time.days) # Availability lag time in days + lag_time = utcnow - enddate.replace(tzinfo=dt.timezone(offset=dt.timedelta())) + self._lag_time = dt.timedelta(days=lag_time.days) # Availability lag time in days self._time_res = 1 # model constants @@ -140,7 +140,7 @@ def load_weather(self, f=None, *args, **kwargs) -> None: def _load_model_level(self, filename) -> None: """Get the variables from the GMAO link using OpenDAP.""" # adding the import here should become absolute when transition to netcdf - ds = xarray.load_dataset(filename) + ds = xr.load_dataset(filename) lons = ds['longitude'].values lats = ds['latitude'].values h = ds['h'].values diff --git a/tools/RAiDER/models/ncmr.py b/tools/RAiDER/models/ncmr.py index 03c0e2c12..f448c727b 100755 --- a/tools/RAiDER/models/ncmr.py +++ b/tools/RAiDER/models/ncmr.py @@ -3,7 +3,7 @@ Modified by Yang Lei, GPS/Caltech """ -import datetime +import datetime as dt import os import urllib.request @@ -38,11 +38,11 @@ def __init__(self) -> None: # Tuple of min/max years where data is available. self._valid_range = ( - datetime.datetime(2015, 12, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc), + dt.datetime(2015, 12, 1).replace(tzinfo=dt.timezone(offset=dt.timedelta())), + dt.datetime.now(dt.timezone.utc), ) # Availability lag time in days/hours - self._lag_time = datetime.timedelta(hours=6) + self._lag_time = dt.timedelta(hours=6) # model constants self._k1 = 0.776 # [K/Pa] diff --git a/tools/RAiDER/models/template.py b/tools/RAiDER/models/template.py index 82ba5f0a5..e7b2cf7b0 100644 --- a/tools/RAiDER/models/template.py +++ b/tools/RAiDER/models/template.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt import numpy as np from pyproj import CRS @@ -20,11 +20,11 @@ def __init__(self) -> None: # Tuple of min/max years where data is available. # valid range of the dataset. Users need to specify the start date and end date (can be "present") self._valid_range = ( - datetime.datetime(2016, 7, 15).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc), + dt.datetime(2016, 7, 15).replace(tzinfo=dt.timezone(offset=dt.timedelta())), + dt.datetime.now(dt.timezone.utc), ) # Availability lag time. Can be specified in hours "hours=3" or in days "days=3" - self._lag_time = datetime.timedelta(hours=3) + self._lag_time = dt.timedelta(hours=3) # Availabile time resolution; i.e. minimum rate model is available in hours. 1 is hourly self._time_res = 1 diff --git a/tools/RAiDER/models/weatherModel.py b/tools/RAiDER/models/weatherModel.py index 9bdfc7c7b..c567001d6 100755 --- a/tools/RAiDER/models/weatherModel.py +++ b/tools/RAiDER/models/weatherModel.py @@ -1,10 +1,10 @@ -import datetime +import datetime as dt import os from abc import ABC, abstractmethod from typing import Optional import numpy as np -import xarray +import xarray as xr from pyproj import CRS from shapely.affinity import translate from shapely.geometry import box @@ -62,10 +62,10 @@ def __init__(self) -> None: self._model_level_type = 'ml' self._valid_range = ( - datetime.datetime(1900, 1, 1).replace(tzinfo=datetime.timezone(offset=datetime.timedelta())), - datetime.datetime.now(datetime.timezone.utc).date(), + dt.datetime(1900, 1, 1).replace(tzinfo=dt.timezone(offset=dt.timedelta())), + dt.datetime.now(dt.timezone.utc).date(), ) # Tuple of min/max years where data is available. - self._lag_time = datetime.timedelta(days=30) # Availability lag time in days + self._lag_time = dt.timedelta(days=30) # Availability lag time in days self._time = None self._bbox = None @@ -169,13 +169,13 @@ def getTime(self): def setTime(self, time, fmt='%Y-%m-%dT%H:%M:%S') -> None: """Set the time for a weather model.""" if isinstance(time, str): - self._time = datetime.datetime.strptime(time, fmt) - elif isinstance(time, datetime.datetime): + self._time = dt.datetime.strptime(time, fmt) + elif isinstance(time, dt.datetime): self._time = time else: raise ValueError('"time" must be a string or a datetime object') if self._time.tzinfo is None: - self._time = self._time.replace(tzinfo=datetime.timezone(offset=datetime.timedelta())) + self._time = self._time.replace(tzinfo=dt.timezone(offset=dt.timedelta())) def get_latlon_bounds(self): return self._ll_bounds @@ -280,12 +280,12 @@ def checkTime(self, time) -> None: start_time = self._valid_range[0] end_time = self._valid_range[1] - if not isinstance(time, datetime.datetime): + if not isinstance(time, dt.datetime): raise ValueError(f'"time" should be a Python datetime object, instead it is {time}') # This is needed because Python now gets angry if you try to compare non-timezone-aware # objects with time-zone aware objects. - time = time.replace(tzinfo=datetime.timezone(offset=datetime.timedelta())) + time = time.replace(tzinfo=dt.timezone(offset=dt.timedelta())) logger.info('Weather model %s is available from %s to %s', self.Model(), start_time, end_time) if time < start_time: @@ -294,9 +294,9 @@ def checkTime(self, time) -> None: if end_time < time: raise DatetimeOutsideRange(self.Model(), time) - # datetime.datetime.utcnow() is deprecated because Python developers + # dt.datetime.utcnow() is deprecated because Python developers # want everyone to use timezone-aware datetimes. - if time > datetime.datetime.now(datetime.timezone.utc) - self._lag_time: + if time > dt.datetime.now(dt.timezone.utc) - self._lag_time: raise DatetimeOutsideRange(self.Model(), time) def setLevelType(self, levelType) -> None: @@ -426,7 +426,7 @@ def bbox(self) -> list: if not os.path.exists(path_weather_model): raise ValueError('Need to save cropped weather model as netcdf') - with xarray.load_dataset(path_weather_model) as ds: + with xr.load_dataset(path_weather_model) as ds: try: xmin, xmax = ds.x.min(), ds.x.max() ymin, ymax = ds.y.min(), ds.y.max() @@ -643,8 +643,8 @@ def write(self): attrs_dict = { 'Conventions': 'CF-1.6', - 'datetime': datetime.datetime.strftime(self._time, '%Y_%m_%dT%H_%M_%S'), - 'date_created': datetime.datetime.now().strftime('%Y_%m_%dT%H_%M_%S'), + 'datetime': dt.datetime.strftime(self._time, '%Y_%m_%dT%H_%M_%S'), + 'date_created': dt.datetime.now().strftime('%Y_%m_%dT%H_%M_%S'), 'title': 'Weather model data and delay calculations', 'model_name': self._Name, } @@ -668,7 +668,7 @@ def write(self): 'hydro_total': (('z', 'y', 'x'), self._hydrostatic_ztd.swapaxes(0, 2).swapaxes(1, 2)), } - ds = xarray.Dataset(data_vars=dataset_dict, coords=dimension_dict, attrs=attrs_dict) + ds = xr.Dataset(data_vars=dataset_dict, coords=dimension_dict, attrs=attrs_dict) # Define units ds['t'].attrs['units'] = 'K' @@ -717,7 +717,7 @@ def make_weather_model_filename(name, time, ll_bounds) -> str: def make_raw_weather_data_filename(outLoc, name, time): """Filename generator for the raw downloaded weather model data.""" - date_string = datetime.datetime.strftime(time, '%Y_%m_%d_T%H_%M_%S') + date_string = dt.datetime.strftime(time, '%Y_%m_%d_T%H_%M_%S') f = os.path.join(outLoc, f'{name}_{date_string}.nc') return f diff --git a/tools/RAiDER/s1_azimuth_timing.py b/tools/RAiDER/s1_azimuth_timing.py index 164fdefda..31530a678 100644 --- a/tools/RAiDER/s1_azimuth_timing.py +++ b/tools/RAiDER/s1_azimuth_timing.py @@ -1,4 +1,4 @@ -import datetime +import datetime as dt import warnings import asf_search as asf @@ -18,8 +18,8 @@ def _asf_query( point: Point, - start: datetime.datetime, - end: datetime.datetime, + start: dt.datetime, + end: dt.datetime, buffer_degrees: float = 2 ) -> list[str]: """ @@ -28,8 +28,8 @@ def _asf_query( Parameters ---------- point : Point - start : datetime.datetime - end : datetime.datetime + start : dt.datetime + end : dt.datetime buffer_degrees : float, optional Returns: @@ -50,7 +50,7 @@ def _asf_query( def get_slc_id_from_point_and_time( lon: float, lat: float, - dt: datetime.datetime, + datetime: dt.datetime, buffer_seconds: int = 600, buffer_deg: float = 2 ) -> list: @@ -62,7 +62,7 @@ def get_slc_id_from_point_and_time( ---------- lon : float lat : float - dt : datetime.datetime + datetime : dt.datetime buffer_seconds : int, optional Do not recommend adjusting this, by default 600, to ensure enough padding for multiple orbit files @@ -72,9 +72,9 @@ def get_slc_id_from_point_and_time( All slc_ids returned by asf_search """ point = Point(lon, lat) - time_delta = datetime.timedelta(seconds=buffer_seconds) - start = dt - time_delta - end = dt + time_delta + time_delta = dt.timedelta(seconds=buffer_seconds) + start = datetime - time_delta + end = datetime + time_delta # Requires buffer of degrees to get several SLCs and ensure we get correct # orbit files @@ -151,7 +151,7 @@ def get_s1_azimuth_time_grid( lon: np.ndarray, lat: np.ndarray, hgt: np.ndarray, - dt: datetime.datetime + datetime: dt.datetime ) -> np.ndarray: """Based on the lon, lat, hgt (3d cube) - obtains an associated s1 orbit file to calculate the azimuth timing across the cube. Requires datetime of acq @@ -165,7 +165,7 @@ def get_s1_azimuth_time_grid( 1 dimensional coordinate array or 3d mesh of coordinates hgt : np.ndarray 1 dimensional coordinate array or 3d mesh of coordinates - dt : datetime.datetime + datetime : dt.datetime Returns: ------- @@ -197,7 +197,7 @@ def get_s1_azimuth_time_grid( try: lon_m = np.mean(lon) lat_m = np.mean(lat) - slc_ids = get_slc_id_from_point_and_time(lon_m, lat_m, dt) + slc_ids = get_slc_id_from_point_and_time(lon_m, lat_m, datetime) except ValueError: warnings.warn('No slc id found for the given datetime and grid; returning empty grid') m, n, p = hgt_mesh.shape @@ -207,35 +207,35 @@ def get_s1_azimuth_time_grid( orb_files = get_orbits_from_slc_ids_hyp3lib(slc_ids) orb_files = [str(of) for of in orb_files] - orb = get_isce_orbit(orb_files, dt, pad=600) + orb = get_isce_orbit(orb_files, datetime, pad=600) az_arr = get_azimuth_time_grid(lon_mesh, lat_mesh, hgt_mesh, orb) return az_arr def get_n_closest_datetimes( - ref_time: datetime.datetime, + ref_time: dt.datetime, n_target_times: int, time_step_hours: int -) -> list[datetime.datetime]: +) -> list[dt.datetime]: """ Gets n closest times relative to the `round_to_hour_delta` and the `ref_time`. Specifically, if one is interetsted in getting 3 closest times to say 0, 6, 12, 18 UTC times of a ref time `dt`, then: ``` - dt = datetime.datetime(2023, 1, 1, 11, 0, 0) + dt = dt.datetime(2023, 1, 1, 11, 0, 0) get_n_closest_datetimes(dt, 3, 6) ``` gives the desired answer of ``` - [datetime.datetime(2023, 1, 1, 12, 0, 0), - datetime.datetime(2023, 1, 1, 6, 0, 0), - datetime.datetime(2023, 1, 1, 18, 0, 0)] + [dt.datetime(2023, 1, 1, 12, 0, 0), + dt.datetime(2023, 1, 1, 6, 0, 0), + dt.datetime(2023, 1, 1, 18, 0, 0)] ``` Parameters ---------- - ref_time : datetime.datetime + ref_time : dt.datetime Time to round from n_times : int Number of times to get @@ -246,7 +246,7 @@ def get_n_closest_datetimes( Returns: ------- - list[datetime.datetime] + list[dt.datetime] List of closest dates ordered by absolute proximity. If two dates have same distance to ref_time, choose earlier one (more likely to be available) """ @@ -278,38 +278,38 @@ def get_n_closest_datetimes( def get_times_for_azimuth_interpolation( - ref_time: datetime.datetime, + ref_time: dt.datetime, time_step_hours: int, buffer_in_seconds: int = 300 -) -> list[datetime.datetime]: +) -> list[dt.datetime]: """Obtains times needed for azimuth interpolation. Filters 3 closests dates from ref_time so that all returned dates are within `time_step_hours` + `buffer_in_seconds`. This ensures we request dates that are really needed. ``` - dt = datetime.datetime(2023, 1, 1, 11, 1, 0) + dt = dt.datetime(2023, 1, 1, 11, 1, 0) get_times_for_azimuth_interpolation(dt, 1) ``` yields ``` - [datetime.datetime(2023, 1, 1, 11, 0, 0), - datetime.datetime(2023, 1, 1, 12, 0, 0), - datetime.datetime(2023, 1, 1, 10, 0, 0)] + [dt.datetime(2023, 1, 1, 11, 0, 0), + dt.datetime(2023, 1, 1, 12, 0, 0), + dt.datetime(2023, 1, 1, 10, 0, 0)] ``` whereas ``` - dt = datetime.datetime(2023, 1, 1, 11, 30, 0) + dt = dt.datetime(2023, 1, 1, 11, 30, 0) get_times_for_azimuth_interpolation(dt, 1) ``` yields ``` - [datetime.datetime(2023, 1, 1, 11, 0, 0), - datetime.datetime(2023, 1, 1, 12, 0, 0)] + [dt.datetime(2023, 1, 1, 11, 0, 0), + dt.datetime(2023, 1, 1, 12, 0, 0)] ``` Parameters ---------- - ref_time : datetime.datetime + ref_time : dt.datetime A time of acquisition time_step_hours : int Weather model time step, should evenly divide 24 hours @@ -318,13 +318,13 @@ def get_times_for_azimuth_interpolation( Returns: ------- - list[datetime.datetime] + list[dt.datetime] 2 or 3 closest times within 1 time step (plust the buffer) and the reference time """ # Get 3 closest times closest_times = get_n_closest_datetimes(ref_time, 3, time_step_hours) - def filter_time(time: datetime.datetime): + def filter_time(time: dt.datetime): absolute_time_difference_sec = abs((ref_time - time).total_seconds()) upper_bound_seconds = time_step_hours * 60 * 60 + buffer_in_seconds return absolute_time_difference_sec < upper_bound_seconds @@ -335,7 +335,7 @@ def filter_time(time: datetime.datetime): def get_inverse_weights_for_dates( azimuth_time_array: np.ndarray, - dates: list[datetime.datetime], + dates: list[dt.datetime], inverse_regularizer: float = 1e-9, temporal_window_hours: float = None, ) -> list[np.ndarray]: @@ -350,7 +350,7 @@ def get_inverse_weights_for_dates( ---------- azimuth_time_array : np.ndarray Array of type `np.datetime64[ms]` - dates : list[datetime.datetime] + dates : list[dt.datetime] List of datetimes inverse_regularizer : float, optional If a `time` in the azimuth time arr equals one of the given dates, then the regularlizer ensures that the value @@ -373,7 +373,7 @@ def get_inverse_weights_for_dates( if n_dates == 0: raise ValueError('No dates provided') - if not all([isinstance(date, datetime.datetime) for date in dates]): + if not all([isinstance(date, dt.datetime) for date in dates]): raise TypeError('dates must be all datetimes') if temporal_window_hours is None: temporal_window_seconds = min([abs((date - dates[0]).total_seconds()) for date in dates[1:]]) diff --git a/tools/RAiDER/s1_orbits.py b/tools/RAiDER/s1_orbits.py index e88bc807d..217a1191d 100644 --- a/tools/RAiDER/s1_orbits.py +++ b/tools/RAiDER/s1_orbits.py @@ -1,3 +1,4 @@ +import datetime as dt import netrc import os import re @@ -107,25 +108,25 @@ def get_orbits_from_slc_ids_hyp3lib(slc_ids: list, orbit_directory: str = None) return orbits -def download_eofs(dts: list, missions: list, save_dir: str): +def download_eofs(datetimes: list[dt.datetime], missions: list, save_dir: str): """Wrapper around sentineleof to first try downloading from ASF and fall back to CDSE.""" _ = ensure_orbit_credentials() orb_files = [] - for dt, mission in zip(dts, missions): - dt = dt if isinstance(dt, list) else [dt] + for datetime, mission in zip(datetimes, missions): + datetime = datetime if isinstance(datetime, list) else [datetime] mission = mission if isinstance(mission, list) else [mission] try: - orb_file = eof.download.download_eofs(dt, mission, save_dir=save_dir, force_asf=True) + orb_file = eof.download.download_eofs(datetime, mission, save_dir=save_dir, force_asf=True) except: logger.error('Could not download orbit from ASF, trying ESA...') - orb_file = eof.download.download_eofs(dt, mission, save_dir=save_dir, force_asf=False) + orb_file = eof.download.download_eofs(datetime, mission, save_dir=save_dir, force_asf=False) orb_file = orb_file[0] if isinstance(orb_file, list) else orb_file orb_files.append(orb_file) - if not len(orb_files) == len(dts): - raise Exception(f'Missing {len(dts) - len(orb_files)} orbit files! dts={dts}, orb_files={len(orb_files)}') + if not len(orb_files) == len(datetimes): + raise Exception(f'Missing {len(datetimes) - len(orb_files)} orbit files! dts={datetimes}, orb_files={len(orb_files)}') return orb_files diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 58a653c30..6e9ae9cd5 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -1,14 +1,14 @@ """Geodesy-related utility functions.""" +import datetime as dt import pathlib import re -from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Optional, Union import numpy as np import rasterio -import xarray +import xarray as xr import yaml from numpy import ndarray from pyproj import CRS, Proj, Transformer @@ -288,15 +288,15 @@ def writeArrayToRaster( def round_date(date, precision): # First try rounding up # Timedelta since the beginning of time - T0 = datetime.min + T0 = dt.datetime.min try: datedelta = T0 - date except TypeError: - T0 = T0.replace(tzinfo=timezone(offset=timedelta())) + T0 = T0.replace(tzinfo=dt.timezone(offset=dt.timedelta())) datedelta = T0 - date - # Round that timedelta to the specified precision + # Round that dt.timedelta to the specified precision rem = datedelta % precision # Add back to get date rounded up round_up = date + rem @@ -305,7 +305,7 @@ def round_date(date, precision): try: datedelta = date - T0 except TypeError: - T0 = T0.replace(tzinfo=timezone(offset=timedelta())) + T0 = T0.replace(tzinfo=dt.timezone(offset=dt.timedelta())) datedelta = date - T0 rem = datedelta % precision @@ -403,16 +403,16 @@ def padLower(invar): return np.concatenate((new_var[:, :, np.newaxis], invar), axis=2) -def round_time(dt, roundTo=60): +def round_time(datetime, roundTo=60): """ Round a datetime object to any time lapse in seconds - dt: datetime.datetime object + datetime: dt.datetime object roundTo: Closest number of seconds to round to, default 1 minute. Source: https://stackoverflow.com/questions/3463930/how-to-round-the-minute-of-a-datetime-object/10854034#10854034 """ - seconds = (dt.replace(tzinfo=None) - dt.min).seconds + seconds = (datetime.replace(tzinfo=None) - datetime.min).seconds rounding = (seconds + roundTo / 2) // roundTo * roundTo - return dt + timedelta(0, rounding - seconds, -dt.microsecond) + return datetime + dt.timedelta(0, rounding - seconds, -datetime.microsecond) def writeDelays( @@ -456,7 +456,7 @@ def getTimeFromFile(filename): fmt = '%Y_%m_%d_T%H_%M_%S' p = re.compile(r'\d{4}_\d{2}_\d{2}_T\d{2}_\d{2}_\d{2}') out = p.search(filename).group() - return datetime.strptime(out, fmt) + return dt.datetime.strptime(out, fmt) # Part of the following UTM and WGS84 converter is borrowed from https://gist.github.com/twpayne/4409500 @@ -614,10 +614,10 @@ def requests_retry_session(retries=10, session=None): return session -def writeWeatherVarsXarray(lat, lon, h, q, p, t, dt, crs, outName=None, NoDataValue=-9999, chunk=(1, 91, 144)) -> None: +def writeWeatherVarsXarray(lat, lon, h, q, p, t, datetime, crs, outName=None, NoDataValue=-9999, chunk=(1, 91, 144)) -> None: # I added datetime as an input to the function and just copied these two lines from merra2 for the attrs_dict attrs_dict = { - 'datetime': dt.strftime('%Y_%m_%dT%H_%M_%S'), + 'datetime': datetime.strftime('%Y_%m_%dT%H_%M_%S'), 'date_created': datetime.now().strftime('%Y_%m_%dT%H_%M_%S'), 'NoDataValue': NoDataValue, 'chunksize': chunk, @@ -636,7 +636,7 @@ def writeWeatherVarsXarray(lat, lon, h, q, p, t, dt, crs, outName=None, NoDataVa 't': (('z', 'y', 'x'), t), } - ds = xarray.Dataset( + ds = xr.Dataset( data_vars=dataset_dict, coords=dimension_dict, attrs=attrs_dict, @@ -811,11 +811,10 @@ def transform_coords(proj1, proj2, x, y): def get_nearest_wmtimes(t0, time_delta): - """ " - Get the nearest two available times to the requested time given a time step. + """Get the nearest two available times to the requested time given a time step. Args: - t0 - user-requested Python datetime + t0 - user-requested Python datetime time_delta - time interval of weather model Returns: @@ -823,18 +822,18 @@ def get_nearest_wmtimes(t0, time_delta): available times to the requested time Example: - >>> import datetime + >>> import datetime as dt >>> from RAiDER.utilFcns import get_nearest_wmtimes - >>> t0 = datetime.datetime(2020,1,1,11,35,0) + >>> t0 = dt.datetime(2020,1,1,11,35,0) >>> get_nearest_wmtimes(t0, 3) - (datetime.datetime(2020, 1, 1, 9, 0), datetime.datetime(2020, 1, 1, 12, 0)) + (dt.datetime(2020, 1, 1, 9, 0), dt.datetime(2020, 1, 1, 12, 0)) """ # get the closest time available tclose = round_time(t0, roundTo=time_delta * 60 * 60) # Just calculate both options and take the closest - t2_1 = tclose + timedelta(hours=time_delta) - t2_2 = tclose - timedelta(hours=time_delta) + t2_1 = tclose + dt.timedelta(hours=time_delta) + t2_2 = tclose - dt.timedelta(hours=time_delta) t2 = [t2_1 if get_dt(t2_1, t0) < get_dt(t2_2, t0) else t2_2][0] # If you're within 5 minutes just take the closest time @@ -859,9 +858,9 @@ def get_dt(t1, t2): Absolute difference in seconds between the two inputs Examples: - >>> import datetime + >>> import datetime as dt >>> from RAiDER.utilFcns import get_dt - >>> get_dt(datetime.datetime(2020,1,1,5,0,0), datetime.datetime(2020,1,1,0,0,0)) + >>> get_dt(dt.datetime(2020,1,1,5,0,0), dt.datetime(2020,1,1,0,0,0)) 18000.0 """ return np.abs((t1 - t2).total_seconds()) From 73bc0aac71dd4a3ef80c05e2e6ad30780d37dcf6 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:03:57 -0500 Subject: [PATCH 73/76] Formatting - Use start non-const variables with lowercase letter --- tools/RAiDER/models/weatherModel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/RAiDER/models/weatherModel.py b/tools/RAiDER/models/weatherModel.py index c567001d6..d561aa8b2 100755 --- a/tools/RAiDER/models/weatherModel.py +++ b/tools/RAiDER/models/weatherModel.py @@ -154,8 +154,8 @@ def fetch(self, out, time) -> None: # write the error raised by the weather model API to the log try: self._fetch(out) - except Exception as E: - logger.exception(E) + except Exception as e: + logger.exception(e) raise @abstractmethod From a805368892c119d00422dab516e5e7beab703914 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:21:58 -0500 Subject: [PATCH 74/76] Reorganize types.py files --- test/test_checkArgs.py | 2 +- test/test_validators.py | 2 +- tools/RAiDER/aria/prepFromGUNW.py | 3 ++- tools/RAiDER/aria/types.py | 28 ++++++++++++++++++++++++++ tools/RAiDER/checkArgs.py | 2 +- tools/RAiDER/cli/raider.py | 7 ++++--- tools/RAiDER/cli/{args.py => types.py} | 5 +++-- tools/RAiDER/cli/validators.py | 2 +- tools/RAiDER/delay.py | 2 -- tools/RAiDER/gnss/types.py | 13 ++++++++++++ tools/RAiDER/llreader.py | 5 ++--- tools/RAiDER/types/__init__.py | 26 +----------------------- 12 files changed, 57 insertions(+), 40 deletions(-) create mode 100644 tools/RAiDER/aria/types.py rename tools/RAiDER/cli/{args.py => types.py} (97%) create mode 100644 tools/RAiDER/gnss/types.py diff --git a/test/test_checkArgs.py b/test/test_checkArgs.py index 397d0aa7b..cf2f031c4 100644 --- a/test/test_checkArgs.py +++ b/test/test_checkArgs.py @@ -7,7 +7,7 @@ import pytest from RAiDER.checkArgs import checkArgs, get_raster_ext, makeDelayFileNames -from RAiDER.cli.args import AOIGroup, DateGroup, HeightGroupUnparsed, LOSGroup, RunConfig, RuntimeGroup, TimeGroup +from RAiDER.cli.types import AOIGroup, DateGroup, HeightGroupUnparsed, LOSGroup, RunConfig, RuntimeGroup, TimeGroup from RAiDER.llreader import BoundingBox, RasterRDR, StationFile from RAiDER.losreader import Zenith from RAiDER.models.gmao import GMAO diff --git a/test/test_validators.py b/test/test_validators.py index 0137cb11d..d8a41aeab 100644 --- a/test/test_validators.py +++ b/test/test_validators.py @@ -8,7 +8,7 @@ from test import TEST_DIR -from RAiDER.cli.args import DateGroupUnparsed, LOSGroupUnparsed, TimeGroup +from RAiDER.cli.types import DateGroupUnparsed, LOSGroupUnparsed, TimeGroup from RAiDER.cli.validators import ( getBufferedExtent, isOutside, isInside, coerce_into_date, diff --git a/tools/RAiDER/aria/prepFromGUNW.py b/tools/RAiDER/aria/prepFromGUNW.py index a6a6a5a60..7b2cb3720 100644 --- a/tools/RAiDER/aria/prepFromGUNW.py +++ b/tools/RAiDER/aria/prepFromGUNW.py @@ -9,6 +9,7 @@ import sys from pathlib import Path +from RAiDER.aria.types import CalcDelaysArgs import numpy as np import pandas as pd import rasterio @@ -21,7 +22,7 @@ from RAiDER.models.hrrr import AK_GEO, HRRR_CONUS_COVERAGE_POLYGON, check_hrrr_dataset_availability from RAiDER.s1_azimuth_timing import get_times_for_azimuth_interpolation from RAiDER.s1_orbits import get_orbits_from_slc_ids_hyp3lib -from RAiDER.types import BB, CalcDelaysArgs, LookDir +from RAiDER.types import BB, LookDir from RAiDER.utilFcns import write_yaml diff --git a/tools/RAiDER/aria/types.py b/tools/RAiDER/aria/types.py new file mode 100644 index 000000000..56034ddd0 --- /dev/null +++ b/tools/RAiDER/aria/types.py @@ -0,0 +1,28 @@ +import argparse +from pathlib import Path +from typing import Optional + +from RAiDER.types import TimeInterpolationMethod + + +class CalcDelaysArgsUnparsed(argparse.Namespace): + bucket: Optional[str] + bucket_prefix: Optional[str] + input_bucket_prefix: Optional[str] + file: Optional[Path] + weather_model: str + api_uid: Optional[str] + api_key: Optional[str] + interpolate_time: TimeInterpolationMethod + output_directory: Path + +class CalcDelaysArgs(argparse.Namespace): + bucket: Optional[str] + bucket_prefix: Optional[str] + input_bucket_prefix: Optional[str] + file: Path + weather_model: str + api_uid: Optional[str] + api_key: Optional[str] + interpolate_time: TimeInterpolationMethod + output_directory: Path diff --git a/tools/RAiDER/checkArgs.py b/tools/RAiDER/checkArgs.py index 0a9b06500..a9844e89e 100644 --- a/tools/RAiDER/checkArgs.py +++ b/tools/RAiDER/checkArgs.py @@ -12,7 +12,7 @@ import pandas as pd import rasterio.drivers as rd -from RAiDER.cli.args import RunConfig +from RAiDER.cli.types import RunConfig from RAiDER.llreader import BoundingBox, StationFile from RAiDER.logger import logger from RAiDER.losreader import LOS, Zenith diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 2cb9c39a4..0d1d64f9b 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -16,7 +16,9 @@ import RAiDER.aria.calcGUNW import RAiDER.aria.prepFromGUNW from RAiDER import aws -from RAiDER.cli.args import ( +from RAiDER.aria.types import CalcDelaysArgs, CalcDelaysArgsUnparsed +from RAiDER.cli.parser import add_cpus, add_out, add_verbose +from RAiDER.cli.types import ( AOIGroup, AOIGroupUnparsed, DateGroupUnparsed, @@ -28,8 +30,8 @@ RuntimeGroup, TimeGroup, ) -from RAiDER.cli.parser import add_cpus, add_out, add_verbose from RAiDER.cli.validators import DateListAction, date_type +from RAiDER.gnss.types import RAiDERCombineArgs from RAiDER.logger import logger, logging from RAiDER.losreader import Raytracing from RAiDER.models.allowed import ALLOWED_MODELS @@ -39,7 +41,6 @@ get_s1_azimuth_time_grid, get_times_for_azimuth_interpolation, ) -from RAiDER.types import CalcDelaysArgs, CalcDelaysArgsUnparsed from RAiDER.utilFcns import get_dt diff --git a/tools/RAiDER/cli/args.py b/tools/RAiDER/cli/types.py similarity index 97% rename from tools/RAiDER/cli/args.py rename to tools/RAiDER/cli/types.py index 088e2d4f5..73ad5ce19 100644 --- a/tools/RAiDER/cli/args.py +++ b/tools/RAiDER/cli/types.py @@ -84,8 +84,8 @@ def __init__( ) else: sentinel_datetime = dt.datetime.combine(dt.date(1900, 1, 1), self.time) - end_time = sentinel_datetime + dt.timedelta(seconds=TimeGroup._DEFAULT_ACQUISITION_WINDOW_SEC) - self.end_time = end_time.time() + new_end_time = sentinel_datetime + dt.timedelta(seconds=TimeGroup._DEFAULT_ACQUISITION_WINDOW_SEC) + self.end_time = new_end_time.time() if self.end_time < self.time: raise ValueError( 'Acquisition start time must be before end time. ' @@ -95,6 +95,7 @@ def __init__( @staticmethod def coerce_into_time(val: Union[int, str]) -> dt.time: + val = str(val) all_formats = map(''.join, itertools.product(TimeGroup.TIME_FORMATS, TimeGroup.TIMEZONE_FORMATS)) for tf in all_formats: try: diff --git a/tools/RAiDER/cli/validators.py b/tools/RAiDER/cli/validators.py index 36d3d9cfa..dda62ec54 100755 --- a/tools/RAiDER/cli/validators.py +++ b/tools/RAiDER/cli/validators.py @@ -15,7 +15,7 @@ else: Self = Any -from RAiDER.cli.args import ( +from RAiDER.cli.types import ( AOIGroupUnparsed, DateGroup, DateGroupUnparsed, diff --git a/tools/RAiDER/delay.py b/tools/RAiDER/delay.py index 24a9538b9..ff7f34368 100755 --- a/tools/RAiDER/delay.py +++ b/tools/RAiDER/delay.py @@ -17,8 +17,6 @@ import os from typing import Optional, Union -from RAiDER.types import CRSLike -from RAiDER.utilFcns import parse_crs import numpy as np import pyproj import xarray as xr diff --git a/tools/RAiDER/gnss/types.py b/tools/RAiDER/gnss/types.py new file mode 100644 index 000000000..42e097a71 --- /dev/null +++ b/tools/RAiDER/gnss/types.py @@ -0,0 +1,13 @@ +import argparse +from pathlib import Path +from typing import Optional + + +class RAiDERCombineArgs(argparse.Namespace): + raider_file: Path + raider_folder: Path + gnss_folder: Path + gnss_file: Optional[Path] + raider_column_name: str + out_name: str + local_time: Optional[str] diff --git a/tools/RAiDER/llreader.py b/tools/RAiDER/llreader.py index ca765f107..bd6220dc7 100644 --- a/tools/RAiDER/llreader.py +++ b/tools/RAiDER/llreader.py @@ -7,10 +7,8 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import os from pathlib import Path -from typing import Any, Optional, Union +from typing import Optional, Union -from RAiDER.constants import _CUBE_SPACING_IN_M -from RAiDER.types import BB, RIO import numpy as np import pyproj import xarray as xr @@ -24,6 +22,7 @@ from pyproj import CRS from RAiDER.logger import logger +from RAiDER.types import BB, RIO from RAiDER.utilFcns import rio_open, rio_stats diff --git a/tools/RAiDER/types/__init__.py b/tools/RAiDER/types/__init__.py index 6f8c80f9b..58d00736b 100644 --- a/tools/RAiDER/types/__init__.py +++ b/tools/RAiDER/types/__init__.py @@ -1,8 +1,6 @@ """Types specific to RAiDER.""" -import argparse -from pathlib import Path -from typing import Literal, Optional, Union +from typing import Literal, Union from pyproj import CRS @@ -10,25 +8,3 @@ LookDir = Literal['right', 'left'] TimeInterpolationMethod = Literal['none', 'center_time', 'azimuth_time_grid'] CRSLike = Union[CRS, str, int] - -class CalcDelaysArgsUnparsed(argparse.Namespace): - bucket: Optional[str] - bucket_prefix: Optional[str] - input_bucket_prefix: Optional[str] - file: Optional[Path] - weather_model: str - api_uid: Optional[str] - api_key: Optional[str] - interpolate_time: TimeInterpolationMethod - output_directory: Path - -class CalcDelaysArgs(argparse.Namespace): - bucket: Optional[str] - bucket_prefix: Optional[str] - input_bucket_prefix: Optional[str] - file: Path - weather_model: str - api_uid: Optional[str] - api_key: Optional[str] - interpolate_time: TimeInterpolationMethod - output_directory: Path From abd9914c6c06cc99f363e209663ba7d894bebe56 Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:03:35 -0500 Subject: [PATCH 75/76] Free overshadowed test test_check_weather_model_availability Another test was being overshadowed by a test with the same name. Also parametrized/simplified these tests in the process --- test/test_GUNW.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/test/test_GUNW.py b/test/test_GUNW.py index af65edb42..108f047e1 100644 --- a/test/test_GUNW.py +++ b/test/test_GUNW.py @@ -370,6 +370,18 @@ def test_check_weather_model_availability_over_alaska(test_gunw_path_factory, we assert cond +@pytest.mark.parametrize('weather_model_name', ['ERA5', 'GMAO', 'MERRA2', 'HRRR']) +def test_check_weather_model_availability_2(weather_model_name): + gunw_id = Path("test/gunw_test_data/S1-GUNW-D-R-059-tops-20230320_20220418-180300-00179W_00051N-PP-c92e-v2_0_6.nc") + assert check_weather_model_availability(gunw_id, weather_model_name) + + +def test_check_weather_model_availability_3(): + gunw_id = Path("test/gunw_test_data/S1-GUNW-D-R-059-tops-20230320_20220418-180300-00179W_00051N-PP-c92e-v2_0_6.nc") + with pytest.raises(ValueError): + check_weather_model_availability(gunw_id, 'NotAModel') + + @pytest.mark.parametrize('weather_model_name', ['HRRR']) @pytest.mark.parametrize('location', ['california-t71', 'alaska']) def test_weather_model_availability_integration_using_valid_range(location, @@ -653,20 +665,3 @@ def test_get_acq_time_invalid_slc_id(): invalid_slc_id = "test/gunw_azimuth_test_data/S1B_OPER_AUX_POEORB_OPOD_20210731T111940_V20210710T225942_20210712T005942.EOF" with pytest.raises(ValueError): get_acq_time_from_slc_id(invalid_slc_id) - - -def test_check_weather_model_availability(): - gunw_id = "test/gunw_test_data/S1-GUNW-D-R-059-tops-20230320_20220418-180300-00179W_00051N-PP-c92e-v2_0_6.nc" - weather_models = ['ERA5', 'GMAO', 'MERRA2', 'HRRR'] - for wm in weather_models: - assert check_weather_model_availability(gunw_id, wm) - - with pytest.raises(ValueError): - check_weather_model_availability(gunw_id, 'NotAModel') - -def test_check_weather_model_availability_2(): - gunw_id = "test/gunw_test_data/S1-GUNW-D-R-059-tops-20230320_20220418-180300-00179W_00051N-PP-c92e-v2_0_6.nc" - weather_models = ['ERA5', 'GMAO', 'MERRA2', 'HRRR'] - fail_check = [True, True, True, True] - for wm, check in zip(weather_models, fail_check): - assert check_weather_model_availability(gunw_id, wm)==check From b94c1f9002685580279d58e2e55918aa5b561d8a Mon Sep 17 00:00:00 2001 From: Nate Kean <14845347+garlic-os@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:03:47 -0500 Subject: [PATCH 76/76] Use Path, add type annotations --- test/test_GUNW.py | 6 +- test/test_gnss.py | 66 +++++++-------- tools/RAiDER/cli/raider.py | 104 +++++++++++++----------- tools/RAiDER/dem.py | 11 +-- tools/RAiDER/gnss/processDelayFiles.py | 106 ++++++++++++++++--------- tools/RAiDER/gnss/types.py | 3 +- tools/RAiDER/s1_azimuth_timing.py | 3 +- tools/RAiDER/types/BB.py | 4 + tools/RAiDER/utilFcns.py | 2 +- 9 files changed, 169 insertions(+), 136 deletions(-) diff --git a/test/test_GUNW.py b/test/test_GUNW.py index 108f047e1..bc6c52748 100644 --- a/test/test_GUNW.py +++ b/test/test_GUNW.py @@ -638,18 +638,18 @@ def test_check_hrrr_availability_all_true(): assert check_hrrr_dataset_availablity_for_s1_azimuth_time_interpolation(gunw_id) def test_get_slc_ids_from_gunw(): - test_path = 'test/gunw_test_data/S1-GUNW-D-R-059-tops-20230320_20220418-180300-00179W_00051N-PP-c92e-v2_0_6.nc' + test_path = Path('test/gunw_test_data/S1-GUNW-D-R-059-tops-20230320_20220418-180300-00179W_00051N-PP-c92e-v2_0_6.nc') assert get_slc_ids_from_gunw(test_path, 'reference') == 'S1A_IW_SLC__1SDV_20230320T180251_20230320T180309_047731_05BBDB_DCA0.zip' assert get_slc_ids_from_gunw(test_path, 'secondary') == 'S1A_IW_SLC__1SDV_20220418T180246_20220418T180305_042831_051CC3_3C47.zip' with pytest.raises(FileNotFoundError): - get_slc_ids_from_gunw('dummy.nc') + get_slc_ids_from_gunw(Path('dummy.nc')) with pytest.raises(ValueError): get_slc_ids_from_gunw(test_path, 'tertiary') with pytest.raises(OSError): - get_slc_ids_from_gunw('test/weather_files/ERA-5_2020_01_30_T13_52_45_32N_35N_120W_115W.nc') + get_slc_ids_from_gunw(Path('test/weather_files/ERA-5_2020_01_30_T13_52_45_32N_35N_120W_115W.nc')) def test_get_acq_time_valid_slc_id(): diff --git a/test/test_gnss.py b/test/test_gnss.py index 337eb68b9..caa7dddb8 100644 --- a/test/test_gnss.py +++ b/test/test_gnss.py @@ -1,3 +1,4 @@ +from pathlib import Path from RAiDER.models.customExceptions import NoStationDataFoundError from RAiDER.gnss.downloadGNSSDelays import ( get_stats_by_llh, get_station_list, download_tropo_delays, @@ -18,11 +19,9 @@ SCENARIO2_DIR = os.path.join(TEST_DIR, "scenario_2") -def file_len(fname): - with open(fname) as f: - for i, l in enumerate(f): - pass - return i + 1 +def file_len(path: Path) -> int: + with path.open('rb') as f: + return sum(1 for _ in f) @pytest.fixture @@ -39,11 +38,11 @@ def temp_file(): def test_getDateTime(): - f1 = '20080101T060000' - f2 = '20080101T560000' - f3 = '20080101T0600000' - f4 = '20080101_060000' - f5 = '2008-01-01T06:00:00' + f1 = Path('20080101T060000') + f2 = Path('20080101T560000') + f3 = Path('20080101T0600000') + f4 = Path('20080101_060000') + f5 = Path('2008-01-01T06:00:00') assert getDateTime(f1) == datetime.datetime(2008, 1, 1, 6, 0, 0) with pytest.raises(ValueError): getDateTime(f2) @@ -58,10 +57,10 @@ def test_addDateTimeToFiles1(tmp_path, temp_file): df = temp_file with pushd(tmp_path): - new_name = os.path.join(tmp_path, 'tmp.csv') - df.to_csv(new_name, index=False) - addDateTimeToFiles([new_name]) - df = pd.read_csv(new_name) + new_path = tmp_path / 'tmp.csv' + df.to_csv(new_path, index=False) + addDateTimeToFiles([new_path]) + df = pd.read_csv(new_path) assert 'Datetime' not in df.columns @@ -70,13 +69,10 @@ def test_addDateTimeToFiles2(tmp_path, temp_file): df = temp_file with pushd(tmp_path): - new_name = os.path.join( - tmp_path, - 'tmp' + f1 + '.csv' - ) - df.to_csv(new_name, index=False) - addDateTimeToFiles([new_name]) - df = pd.read_csv(new_name) + new_path = tmp_path / f'tmp{f1}.csv' + df.to_csv(new_path, index=False) + addDateTimeToFiles([new_path]) + df = pd.read_csv(new_path) assert 'Datetime' in df.columns @@ -85,25 +81,19 @@ def test_concatDelayFiles(tmp_path, temp_file): df = temp_file with pushd(tmp_path): - new_name = os.path.join( - tmp_path, - 'tmp' + f1 + '.csv' - ) - new_name2 = os.path.join( - tmp_path, - 'tmp' + f1 + '_2.csv' - ) - df.to_csv(new_name, index=False) - df.to_csv(new_name2, index=False) - file_length = file_len(new_name) - addDateTimeToFiles([new_name, new_name2]) - - out_name = os.path.join(tmp_path, 'out.csv') + new_path1 = tmp_path / f'tmp{f1}_1.csv' + new_path2 = tmp_path / f'tmp{f1}_2.csv' + df.to_csv(new_path1, index=False) + df.to_csv(new_path2, index=False) + file_length = file_len(new_path1) + addDateTimeToFiles([new_path1, new_path2]) + + out_path = tmp_path / 'out.csv' concatDelayFiles( - [new_name, new_name2], - outName=out_name + [new_path1, new_path2], + outName=out_path ) - assert file_len(out_name) == file_length + assert file_len(out_path) == file_length def test_get_stats_by_llh2(): diff --git a/tools/RAiDER/cli/raider.py b/tools/RAiDER/cli/raider.py index 0d1d64f9b..ced83c261 100644 --- a/tools/RAiDER/cli/raider.py +++ b/tools/RAiDER/cli/raider.py @@ -41,6 +41,7 @@ get_s1_azimuth_time_grid, get_times_for_azimuth_interpolation, ) +from RAiDER.types import TimeInterpolationMethod from RAiDER.utilFcns import get_dt @@ -197,7 +198,7 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: group.add_argument( 'run_config_file', nargs='?', - type=lambda p: Path(p).absolute(), + type=lambda s: Path(s).absolute(), help='a YAML file with arguments to RAiDER' ) @@ -298,7 +299,7 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: raise NotImplementedError( 'Only none, center_time, and azimuth_time_grid are accepted values for interp_method.' ) - wfiles = [] + wfiles: list[Path] = [] for tt in times: try: wfile = RAiDER.processWM.prepareWeatherModel( @@ -307,7 +308,7 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: aoi.bounds(), makePlots=run_config.runtime_group.verbose ) - wfiles.append(wfile) + wfiles.append(Path(wfile)) except TryToKeepGoingError: if interp_method in ('azimuth_time_grid', 'none'): @@ -336,7 +337,7 @@ def calcDelays(iargs: Optional[Sequence[str]]=None) -> list[Path]: try: wet_delay, hydro_delay = tropo_delay( t, - weather_model_file, + str(weather_model_file), aoi, los, height_levels=run_config.height_group.height_levels, @@ -529,7 +530,7 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> Optional[xr.Dataset]: p.add_argument( '-f', '--file', - type=lambda p: Path(p).absolute(), + type=lambda s: Path(s).absolute(), help='1 ARIA GUNW netcdf file', ) @@ -572,7 +573,7 @@ def calcDelaysGUNW(iargs: Optional[list[str]] = None) -> Optional[xr.Dataset]: '-o', '--output-directory', default=Path.cwd(), - type=lambda p: Path(p).absolute(), + type=lambda s: Path(s).absolute(), help='Directory to store results.' ) @@ -681,34 +682,41 @@ def combineZTDFiles() -> None: from RAiDER.gnss.processDelayFiles import combineDelayFiles, create_parser, main p = create_parser() - args = p.parse_args() + args: RAiDERCombineArgs = p.parse_args(namespace=RAiDERCombineArgs()) - if not os.path.exists(args.raider_file): + if not args.raider_file.exists(): combineDelayFiles(args.raider_file, loc=args.raider_folder) - if not os.path.exists(args.gnss_file): + if args.gnss_file is None: + return + + if not args.gnss_file.exists(): combineDelayFiles( args.gnss_file, loc=args.gnss_folder, source='GNSS', ref=args.raider_file, col_name=args.column_name ) - if args.gnss_file is not None: - main( - args.raider_file, - args.gnss_file, - col_name=args.column_name, - raider_delay=args.raider_column_name, - outName=args.out_name, - localTime=args.local_time, - ) + main( + args.raider_file, + args.gnss_file, + col_name=args.column_name, + raider_delay=args.raider_column_name, + out_path=args.out_name, + local_time=args.local_time, + ) -def getWeatherFile(wfiles, times, t, model, interp_method='none'): - """ - # Time interpolation. - # - # Need to handle various cases, including if the exact weather model time is - # requested, or if one or more datetimes are not available from the weather - # model data provider +def getWeatherFile( + wfiles: list[Path], + times: list, + time: dt.datetime, + model: str, + interp_method: TimeInterpolationMethod='none' +) -> Optional[Path]: + """Time interpolation. + + Need to handle various cases, including if the exact weather model time is + requested, or if one or more datetimes are not available from the weather + model data provider """ # time interpolation method: number of expected files EXPECTED_NUM_FILES = {'none': 1, 'center_time': 2, 'azimuth_time_grid': 3} @@ -735,7 +743,7 @@ def getWeatherFile(wfiles, times, t, model, interp_method='none'): elif interp_method == 'center_time': if Nmatch: # Case 3: two weather files downloaded - weather_model_file = combine_weather_files(wfiles, t, model, interp_method='center_time') + weather_model_file = combine_weather_files(wfiles, time, model, interp_method='center_time') elif Tmatch: # Case 4: Exact time is available without interpolation logger.warning('Time interpolation is not needed as exact time is available') weather_model_file = wfiles[0] @@ -747,24 +755,23 @@ def getWeatherFile(wfiles, times, t, model, interp_method='none'): else: raise WrongNumberOfFiles(Nfiles_expected, Nfiles) - elif (interp_method) == 'azimuth_time_grid': + elif interp_method == 'azimuth_time_grid': if Nmatch or Tmatch: # Case 6: all files downloaded - weather_model_file = combine_weather_files(wfiles, t, model, interp_method='azimuth_time_grid') + weather_model_file = combine_weather_files(wfiles, time, model, interp_method='azimuth_time_grid') else: raise WrongNumberOfFiles(Nfiles_expected, Nfiles) # Case 7 - Anything else errors out else: - N = len(wfiles) raise NotImplementedError( - f'The {interp_method} with {N} retrieved weather model files was not well posed ' + f'The {interp_method} with {len(wfiles)} retrieved weather model files was not well posed ' 'for the current workflow.' ) return weather_model_file -def combine_weather_files(wfiles, t, model, interp_method='center_time'): +def combine_weather_files(wfiles: list[Path], time: dt.datetime, model: str, interp_method: TimeInterpolationMethod='center_time') -> Path: """Interpolate downloaded weather files and save to a single file.""" STYLE = {'center_time': '_timeInterp_', 'azimuth_time_grid': '_timeInterpAziGrid_'} @@ -772,7 +779,7 @@ def combine_weather_files(wfiles, t, model, interp_method='center_time'): datasets = [xr.open_dataset(f) for f in wfiles] # Pull the datetimes from the datasets - times = [] + times: list[dt.datetime] = [] for ds in datasets: times.append(dt.datetime.strptime(ds.attrs['datetime'], '%Y_%m_%dT%H_%M_%S')) @@ -781,10 +788,12 @@ def combine_weather_files(wfiles, t, model, interp_method='center_time'): # calculate relative weights of each dataset if interp_method == 'center_time': - wgts = get_weights_time_interp(times, t) + wgts = get_weights_time_interp(times, time) elif interp_method == 'azimuth_time_grid': - time_grid = get_time_grid_for_aztime_interp(datasets, t, model) + time_grid = get_time_grid_for_aztime_interp(datasets, time, model) wgts = get_inverse_weights_for_dates(time_grid, times) + else: # interp_method == 'none' + raise ValueError('Interpolating weather files is not available with interpolation method "none"') # combine datasets ds_out = datasets[0] @@ -794,13 +803,12 @@ def combine_weather_files(wfiles, t, model, interp_method='center_time'): ds_out.attrs['Date2'] = 0 # Give the weighted combination a new file name - weather_model_file = os.path.join( - os.path.dirname(wfiles[0]), - os.path.basename(wfiles[0]).split('_')[0] + weather_model_file = wfiles[0].parent / ( + wfiles[0].name.split('_')[0] + '_' - + t.strftime('%Y_%m_%dT%H_%M_%S') + + time.strftime('%Y_%m_%dT%H_%M_%S') + STYLE[interp_method] - + '_'.join(wfiles[0].split('_')[-4:]), + + '_'.join(wfiles[0].name.split('_')[-4:]) ) # write the combined results to disk @@ -809,19 +817,19 @@ def combine_weather_files(wfiles, t, model, interp_method='center_time'): return weather_model_file -def combine_files_using_azimuth_time(wfiles, t, times): +def combine_files_using_azimuth_time(wfiles, time: dt.datetime, times: list[dt.datetime]): """Combine files using azimuth time interpolation.""" # read the individual datetime datasets datasets = [xr.open_dataset(f) for f in wfiles] # Pull the datetimes from the datasets - times = [] + times: list[dt.datetime] = [] for ds in datasets: times.append(dt.datetime.strptime(ds.attrs['datetime'], '%Y_%m_%dT%H_%M_%S')) model = datasets[0].attrs['model_name'] - time_grid = get_time_grid_for_aztime_interp(datasets, times, t, model) + time_grid = get_time_grid_for_aztime_interp(datasets, times, time, model) wgts = get_inverse_weights_for_dates(time_grid, times) @@ -837,7 +845,7 @@ def combine_files_using_azimuth_time(wfiles, t, times): os.path.dirname(wfiles[0]), os.path.basename(wfiles[0]).split('_')[0] + '_' - + t.strftime('%Y_%m_%dT%H_%M_%S') + + time.strftime('%Y_%m_%dT%H_%M_%S') + '_timeInterpAziGrid_' + '_'.join(wfiles[0].split('_')[-4:]), ) @@ -848,21 +856,21 @@ def combine_files_using_azimuth_time(wfiles, t, times): return weather_model_file -def get_weights_time_interp(times, t): +def get_weights_time_interp(times: list[dt.datetime], time: dt.datetime) -> Optional[list[float]]: """Calculate weights for time interpolation using simple inverse linear weighting.""" date1, date2 = times - wgts = [1 - get_dt(t, date1) / get_dt(date2, date1), 1 - get_dt(date2, t) / get_dt(date2, date1)] + wgts = [1 - get_dt(time, date1) / get_dt(date2, date1), 1 - get_dt(date2, time) / get_dt(date2, date1)] try: assert np.isclose(np.sum(wgts), 1) except AssertionError: - logger.error('Time interpolation weights do not sum to one; something is off with query datetime: %s', t) + logger.error('Time interpolation weights do not sum to one; something is off with query datetime: %s', time) return None return wgts -def get_time_grid_for_aztime_interp(datasets, t, model): +def get_time_grid_for_aztime_interp(datasets: list[xr.Dataset], time: dt.datetime, model: str) -> np.ndarray: """Calculate the time-varying grid for use with azimuth time interpolation.""" # Each model will require some inspection here # the subsequent s1 azimuth time grid requires dimension @@ -883,7 +891,7 @@ def get_time_grid_for_aztime_interp(datasets, t, model): else: raise NotImplementedError('Azimuth Time is currently only implemented for HRRR') - time_grid = get_s1_azimuth_time_grid(lon, lat, hgt, t) # This is the acq time from loop + time_grid = get_s1_azimuth_time_grid(lon, lat, hgt, time) # This is the acq time from loop if np.any(np.isnan(time_grid)): raise ValueError('The Time Grid return nans meaning no orbit was downloaded.') diff --git a/tools/RAiDER/dem.py b/tools/RAiDER/dem.py index d9db6793a..53922ed8b 100644 --- a/tools/RAiDER/dem.py +++ b/tools/RAiDER/dem.py @@ -6,14 +6,14 @@ # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ from pathlib import Path -from typing import Optional +from typing import Optional, cast import numpy as np import rasterio from dem_stitcher.stitcher import stitch_dem from RAiDER.logger import logger -from RAiDER.types import RIO +from RAiDER.types import BB, RIO from RAiDER.utilFcns import rio_open @@ -51,19 +51,20 @@ def download_dem( # download the dem # inExtent is SNWE # dem-stitcher wants WSEN - bounds = [ + bounds: BB.WSEN = ( np.floor(ll_bounds[2]) - buf, np.floor(ll_bounds[0]) - buf, np.ceil(ll_bounds[3]) + buf, np.ceil(ll_bounds[1]) + buf, - ] + ) zvals, metadata = stitch_dem( - bounds, + list(bounds), dem_name='glo_30', dst_ellipsoidal_height=True, dst_area_or_point='Area', ) + metadata = cast(RIO.Profile, metadata) if writeDEM: with rasterio.open(dem_path, 'w', **metadata) as ds: ds.write(zvals, 1) diff --git a/tools/RAiDER/gnss/processDelayFiles.py b/tools/RAiDER/gnss/processDelayFiles.py index 24eaa4766..4872eb525 100644 --- a/tools/RAiDER/gnss/processDelayFiles.py +++ b/tools/RAiDER/gnss/processDelayFiles.py @@ -1,10 +1,10 @@ import argparse -import glob import datetime as dt import math -import os import re +from pathlib import Path from textwrap import dedent +from typing import Optional import pandas as pd from tqdm import tqdm @@ -13,46 +13,52 @@ pd.options.mode.chained_assignment = None # default='warn' -def combineDelayFiles(outName, loc=os.getcwd(), source='model', ext='.csv', ref=None, col_name='ZTD') -> None: - files = glob.glob(os.path.join(loc, '*' + ext)) +def combineDelayFiles( + out_path: Path, + loc: Path=Path.cwd(), + source: str='model', + ext: str='.csv', + ref: Optional[Path]=None, + col_name: str='ZTD' +) -> None: + file_paths = list(loc.glob('*' + ext)) if source == 'model': print('Ensuring that "Datetime" column exists in files') - addDateTimeToFiles(files) + addDateTimeToFiles(file_paths) # If single file, just copy source - if len(files) == 1: + if len(file_paths) == 1: if source == 'model': import shutil - - shutil.copy(files[0], outName) + shutil.copy(file_paths[0], out_path) else: - files = readZTDFile(files[0], col_name=col_name) + file_paths = readZTDFile(file_paths[0], col_name=col_name) # drop all lines with nans - files.dropna(how='any', inplace=True) + file_paths.dropna(how='any', inplace=True) # drop all duplicate lines - files.drop_duplicates(inplace=True) - files.to_csv(outName, index=False) + file_paths.drop_duplicates(inplace=True) + file_paths.to_csv(out_path, index=False) return print(f'Combining {source} delay files') try: - concatDelayFiles(files, sort_list=['ID', 'Datetime'], outName=outName, source=source) - concatDelayFiles(files, sort_list=['ID', 'Date'], outName=outName, source=source, ref=ref, col_name=col_name) + concatDelayFiles(file_paths, sort_list=['ID', 'Datetime'], outName=out_path, source=source) except: + concatDelayFiles(file_paths, sort_list=['ID', 'Date'], outName=out_path, source=source, ref=ref, col_name=col_name) -def addDateTimeToFiles(fileList, force=False, verbose=False) -> None: +def addDateTimeToFiles(file_paths: list[Path], force: bool=False, verbose: bool=False) -> None: """Run through a list of files and add the datetime of each file as a column.""" print('Adding Datetime to delay files') - for f in tqdm(fileList): - data = pd.read_csv(f) + for path in tqdm(file_paths): + data = pd.read_csv(path) if 'Datetime' in data.columns and not force: if verbose: print( - f'File {f} already has a "Datetime" column, pass' + f'File {path} already has a "Datetime" column, pass' '"force = True" if you want to override and ' 're-process' ) @@ -63,18 +69,17 @@ def addDateTimeToFiles(fileList, force=False, verbose=False) -> None: data.dropna(how='any', inplace=True) # drop all duplicate lines data.drop_duplicates(inplace=True) - data.to_csv(f, index=False) + data.to_csv(path, index=False) except (AttributeError, ValueError): - print(f'File {f} does not contain datetime info, skipping') + print(f'File {path} does not contain datetime info, skipping') del data -def getDateTime(filename): +def getDateTime(path: Path) -> dt.datetime: """Parse a datetime from a RAiDER delay filename.""" - filename = os.path.basename(filename) - dtr = re.compile(r'\d{8}T\d{6}') - dt = dtr.search(filename) - return datetime.datetime.strptime(dt.group(), '%Y%m%dT%H%M%S') + datetime_pattern = re.compile(r'\d{8}T\d{6}') + match = datetime_pattern.search(path.name) + return dt.datetime.strptime(match.group(), '%Y%m%dT%H%M%S') def update_time(row, localTime_hrs): @@ -217,7 +222,20 @@ def readZTDFile(filename, col_name='ZTD'): return data -def create_parser(): +def file_choices(p: argparse.ArgumentParser, choices: tuple[str], s: str) -> Path: + path = Path(s) + if path.suffix not in choices: + p.error(f"File must end with one of {choices}") + return path + +def parse_dir(p: argparse.ArgumentParser, s: str) -> Path: + path = Path(s) + if not path.is_dir(): + p.error("Path must be a directory") + return path + + +def create_parser() -> argparse.ArgumentParser: """Parse command line arguments using argparse.""" p = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, @@ -240,6 +258,7 @@ def create_parser(): delay files. """), required=True, + type=lambda s: file_choices(p, ('csv',), s), ) p.add_argument( '--raiderDir', @@ -250,7 +269,8 @@ def create_parser(): Files should be named with a Datetime in the name and contain the column "ID" as the delay column names. """), - default=os.getcwd(), + type=lambda s: parse_dir(p, s), + default=Path.cwd(), ) p.add_argument( '--gnssDir', @@ -261,7 +281,8 @@ def create_parser(): Files should contain the column "ID" as the delay column names and times should be denoted by the "Date" key. """), - default=os.getcwd(), + type=lambda s: parse_dir(p, s), + default=Path.cwd(), ) p.add_argument( @@ -271,6 +292,7 @@ def create_parser(): Optional .csv file containing GPS Zenith Delays. Should contain columns "ID", "ZTD", and "Datetime" """), default=None, + type=lambda s: file_choices(p, ('csv',), s), ) p.add_argument( @@ -299,9 +321,9 @@ def create_parser(): dest='out_name', help=dedent("""\ Name to use for the combined delay file. Only used with the "--gnss" option - """), - default='Combined_delays.csv', + type=Path, + default=Path('Combined_delays.csv'), ) p.add_argument( @@ -313,7 +335,6 @@ def create_parser(): and within +/- specified hour threshold (2nd argument). By default UTC is passed as is without local-time conversions. Input in 'HH H', e.g. '16 1'" - """), default=None, ) @@ -321,14 +342,21 @@ def create_parser(): return p -def main(raiderFile, ztdFile, col_name='ZTD', raider_delay='totalDelay', outName=None, localTime=None): +def main( + raider_file: Path, + ztd_file: Path, + col_name: str='ZTD', + raider_delay: str='totalDelay', + out_path: Optional[Path]=None, + local_time=None +): """Merge a combined RAiDER delays file with a GPS ZTD delay file.""" - print(f'Merging delay files {raiderFile} and {ztdFile}') - dfr = pd.read_csv(raiderFile, parse_dates=['Datetime']) + print(f'Merging delay files {raider_file} and {ztd_file}') + dfr = pd.read_csv(raider_file, parse_dates=['Datetime']) # drop extra columns expected_data_columns = ['ID', 'Lat', 'Lon', 'Hgt_m', 'Datetime', 'wetDelay', 'hydroDelay', raider_delay] dfr = dfr.drop(columns=[col for col in dfr if col not in expected_data_columns]) - dfz = pd.read_csv(ztdFile, parse_dates=['Date']) + dfz = pd.read_csv(ztd_file, parse_dates=['Date']) if 'Datetime' not in dfz.keys(): dfz.rename(columns={'Date': 'Datetime'}, inplace=True) # drop extra columns @@ -351,8 +379,8 @@ def main(raiderFile, ztdFile, col_name='ZTD', raider_delay='totalDelay', outName # If specified, convert to local-time reference frame WRT 0 longitude common_keys = ['Datetime', 'ID'] - if localTime is not None: - dfr, dfz = local_time_filter(raiderFile, ztdFile, dfr, dfz, localTime) + if local_time is not None: + dfr, dfz = local_time_filter(raider_file, ztd_file, dfr, dfz, local_time) common_keys.append('Localtime') # only pass common locations and times dfz = pass_common_obs(dfr, dfz, localtime='Localtime') @@ -384,11 +412,11 @@ def main(raiderFile, ztdFile, col_name='ZTD', raider_delay='totalDelay', outName print(f'Total number of rows containing NaNs: {dfc[dfc.isna().any(axis=1)].shape[0]}') print('Merge finished') - if outName is None: + if out_path is None: return dfc else: # drop all lines with nans dfc.dropna(how='any', inplace=True) # drop all duplicate lines dfc.drop_duplicates(inplace=True) - dfc.to_csv(outName, index=False) + dfc.to_csv(out_path, index=False) diff --git a/tools/RAiDER/gnss/types.py b/tools/RAiDER/gnss/types.py index 42e097a71..bf537fa81 100644 --- a/tools/RAiDER/gnss/types.py +++ b/tools/RAiDER/gnss/types.py @@ -9,5 +9,6 @@ class RAiDERCombineArgs(argparse.Namespace): gnss_folder: Path gnss_file: Optional[Path] raider_column_name: str - out_name: str + column_name: str + out_name: Path local_time: Optional[str] diff --git a/tools/RAiDER/s1_azimuth_timing.py b/tools/RAiDER/s1_azimuth_timing.py index 31530a678..9dcdef1f8 100644 --- a/tools/RAiDER/s1_azimuth_timing.py +++ b/tools/RAiDER/s1_azimuth_timing.py @@ -1,4 +1,5 @@ import datetime as dt +from typing import Optional import warnings import asf_search as asf @@ -337,7 +338,7 @@ def get_inverse_weights_for_dates( azimuth_time_array: np.ndarray, dates: list[dt.datetime], inverse_regularizer: float = 1e-9, - temporal_window_hours: float = None, + temporal_window_hours: Optional[float] = None, ) -> list[np.ndarray]: """Obtains weights according to inverse weighting with respect to the absolute difference between azimuth timing array and dates. The output will be a list with length equal to that of dates and diff --git a/tools/RAiDER/types/BB.py b/tools/RAiDER/types/BB.py index 92b7f1ec6..260f2de06 100644 --- a/tools/RAiDER/types/BB.py +++ b/tools/RAiDER/types/BB.py @@ -1,3 +1,7 @@ +"""Types to help distinguish different bounding box formats.""" + SNWE = tuple[float, float, float, float] +WSEN = tuple[float, float, float, float] # used in dem_stitcher + SN = tuple[float, float] WE = tuple[float, float] diff --git a/tools/RAiDER/utilFcns.py b/tools/RAiDER/utilFcns.py index 6e9ae9cd5..d028861ab 100644 --- a/tools/RAiDER/utilFcns.py +++ b/tools/RAiDER/utilFcns.py @@ -846,7 +846,7 @@ def get_nearest_wmtimes(t0, time_delta): return [t2, tclose] -def get_dt(t1, t2): +def get_dt(t1: dt.datetime, t2: dt.datetime) -> float: """ Helper function for getting the absolute difference in seconds between two python datetimes.