From b907af5582e5ad0c7e5f604d15f4d7f90c73abed Mon Sep 17 00:00:00 2001 From: Kelsey Bisson <48059682+kelseybisson@users.noreply.github.com> Date: Wed, 23 Feb 2022 09:26:30 -0700 Subject: [PATCH 001/124] Adding argo search and download script --- argo_BGC_class_sandbox | 154 +++++++++++++++++++++++++++++++++++++++++ get_BGC_argo.py | 96 +++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 argo_BGC_class_sandbox create mode 100644 get_BGC_argo.py diff --git a/argo_BGC_class_sandbox b/argo_BGC_class_sandbox new file mode 100644 index 000000000..a0dda63f5 --- /dev/null +++ b/argo_BGC_class_sandbox @@ -0,0 +1,154 @@ +import requests # dependency for icepyx +import pandas as pd # dependency for icepyx? - geopandas +import os +from .dataset import * + +class Argo_bgc(DataSet): + +# Argo data object to search/download (in one function) for BGC Argo data. + +%spatial_extent : list or string +# Spatial extent of interest, provided as a bounding box, list of polygon coordinates, or +# geospatial polygon file. +# Bounding box coordinates should be provided in decimal degrees as +# [lower-left-longitude, lower-left-latitute, upper-right-longitude, upper-right-latitude]. +# Polygon coordinates should be provided as coordinate pairs in decimal degrees as +# [(longitude1, latitude1), (longitude2, latitude2), ... (longitude_n,latitude_n), (longitude1,latitude1)] +# or +# [longitude1, latitude1, longitude2, latitude2, ... longitude_n,latitude_n, longitude1,latitude1]. + +% timeframe: list (2) of start date and end date, in YYYY-MM-DD + +% meas1, meas2 = string listing of argo measurement, e.g., bbp700, chla, temp, psal, doxy + + def __init__(self, shape, timeframe, meas1, meas2, presRange=None): + self.shape = shape + self.bounding_box = shape.extent # call coord standardization method (see icepyx) + self.time_frame = timeframe # call fmt_timerange + self.meas1 = meas1 + self.meas2 = meas2 + self.presrng = presRange + + def download(self, out_path): + baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection' + meas1Query = '?meas_1=' + self.meas1 + meas2Query = '&meas_2=' + self.meas2 + startDateQuery = '&startDate=' + self.time_frame[0].strftime('%Y-%m-%d') + endDateQuery = '&endDate=' + self.time_frame[1].strftime('%Y-%m-%d') + + shapeQuery = '&shape=' + self.shape # might have to process this + if not self.presrng == None: + pressRangeQuery = '&presRange=' + self.presrng + url = baseURL + meas1Query + meas2Query + startDateQuery + endDateQuery + pressRangeQuery + '&bgcOnly=true' + shapeQuery + else: + url = baseURL + meas1Query + meas2Query + startDateQuery + endDateQuery + '&bgcOnly=true' + shapeQuery + resp = requests.get(url) + + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + return "Error: Unexpected response {}".format(resp) + selectionProfiles = resp.json() + + # save selection profiles somewhere + # return selectionProfiles + + # ---------------------------------------------------------------------- + # Properties + + @property + def dataset(self): + """ + Return the short name dataset ID string associated with the query object. + + """ + return self._dset + + @property + def spatial_extent(self): + """ + Return an array showing the spatial extent of the query object. + Spatial extent is returned as an input type (which depends on how + you initially entered your spatial data) followed by the geometry data. + Bounding box data is [lower-left-longitude, lower-left-latitute, upper-right-longitude, upper-right-latitude]. + Polygon data is [[array of longitudes],[array of corresponding latitudes]]. + + """ + + if self.extent_type == "bounding_box": + return ["bounding box", self._spat_extent] + elif self.extent_type == "polygon": + # return ['polygon', self._spat_extent] + # Note: self._spat_extent is a shapely geometry object + return ["polygon", self._spat_extent.exterior.coords.xy] + else: + return ["unknown spatial type", None] + + @property + def dates(self): + """ + Return an array showing the date range of the query object. + Dates are returned as an array containing the start and end datetime objects, inclusive, in that order. + + """ + return [ + self._start.strftime("%Y-%m-%d"), + self._end.strftime("%Y-%m-%d"), + ] # could also use self._start.date() + + @property + def start_time(self): + """ + Return the start time specified for the start date. + NOTE THAT there is no time input for Argo + """ + return self._start.strftime("%H:%M:%S") + + @property + def end_time(self): + """ + Return the end time specified for the end date. + Examples + """ + return self._end.strftime("%H:%M:%S") + +## DO WE NEED AN ORDER VARS CLASS? or the download already puts data into a dataframe .. maybe order vars could +# save data to CSV? +# IF SO, this code may be relevant for it + +tick1 = 0 +tick2 = 0 +for index, value in enumerate(selectionProfiles): + if meas1 not in value['bgcMeasKeys']: + tick1 += 1 + if meas2 not in value['bgcMeasKeys']: + tick2 += 1 +if tick1 == len(selectionProfiles): + print(f'{meas1} not found in selected data') +if tick2 == len(selectionProfiles): + print(f'{meas2} not found in selected data') + +if tick1 < len(selectionProfiles) & tick2 < len(selectionProfiles): + df = json2dataframe(selectionProfiles, measKey='bgcMeas') + +df.head() + +# NEED TO ADD CODE for the visualization here +def visualize_spatial_extent( + self, + ): # additional args, basemap, zoom level, cmap, export + """ + Creates a map displaying the input spatial extent + Examples + -------- + >>> icepyx.query.Query('ATL06','path/spatialfile.shp',['2019-02-22','2019-02-28']) + >>> reg_a.visualize_spatial_extent + [visual map output] + """ + + world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) + f, ax = plt.subplots(1, figsize=(12, 6)) + world.plot(ax=ax, facecolor="lightgray", edgecolor="gray") + geospatial.geodataframe(self.extent_type, self._spat_extent).plot( + ax=ax, color="#FF8C00", alpha=0.7 + ) + plt.show() \ No newline at end of file diff --git a/get_BGC_argo.py b/get_BGC_argo.py new file mode 100644 index 000000000..cc30b986e --- /dev/null +++ b/get_BGC_argo.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu Oct 29 13:50:54 2020 + +@author: bissonk +""" + +# made for Python 3. It may work with Python 2.7, but has not been well tested + +# libraries to call for all python API calls on Argovis + +import requests +import pandas as pd +import os + +##### +# Get current directory to save file into + +curDir = os.getcwd() + +# Get a selected region from Argovis + + +def get_selection_profiles(startDate, endDate, shape, meas1,meas2, presRange=None): + baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection' + meas1Query = '?meas_1=' + meas1 + meas2Query = '&meas_2=' + meas2 + startDateQuery = '&startDate=' + startDate + endDateQuery = '&endDate=' + endDate + shapeQuery = '&shape='+shape + if not presRange == None: + pressRangeQuery = '&presRange=' + presRange + url = baseURL + meas1Query + meas2Query + startDateQuery + endDateQuery + pressRangeQuery + '&bgcOnly=true' + shapeQuery + else: + url = baseURL + meas1Query + meas2Query + startDateQuery + endDateQuery + '&bgcOnly=true' + shapeQuery + resp = requests.get(url) + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + return "Error: Unexpected response {}".format(resp) + selectionProfiles = resp.json() + return selectionProfiles + + +def json2dataframe(selectionProfiles, measKey='measurements'): + """ convert json data to Pandas DataFrame """ + # Make sure we deal with a list + if isinstance(selectionProfiles, list): + data = selectionProfiles + else: + data = [selectionProfiles] + # Transform + rows = [] + for profile in data: + keys = [x for x in profile.keys() if x not in ['measurements', 'bgcMeas']] + meta_row = dict((key, profile[key]) for key in keys) + for row in profile[measKey]: + row.update(meta_row) + rows.append(row) + df = pd.DataFrame(rows) + return df +# set start date, end date, lat/lon coordinates for the shape of region and pres range + +startDate='2020-10-08' +endDate='2020-10-22' +# shape should be nested array with lon, lat coords. +shape = '[[[-49.21875,48.806863],[-55.229808,54.85326],[-63.28125,60.500525],[-60.46875,64.396938],[-49.746094,61.185625],[-38.496094,54.059388],[-41.484375,47.754098],[-49.21875,48.806863]]]' +presRange='[0,30]' +meas1 = 'bbp700' +meas2 = 'chla' + +meas1= 'temp' +meas2='psal' +# tested with meas1 = temp, meas2 = psal and it works + +selectionProfiles = get_selection_profiles(startDate, endDate, shape, meas1, meas2, presRange=None) + + +# loop thru profiles and search for measurement +tick1 = 0 +tick2 = 0 +for index, value in enumerate(selectionProfiles): + if meas1 not in value['bgcMeasKeys']: + tick1 += 1 + if meas2 not in value['bgcMeasKeys']: + tick2 += 1 +if tick1 == len(selectionProfiles): + print(f'{meas1} not found in selected data') +if tick2 == len(selectionProfiles): + print(f'{meas2} not found in selected data') + +if tick1 < len(selectionProfiles) & tick2 < len(selectionProfiles): + df = json2dataframe(selectionProfiles, measKey='bgcMeas') + +df.head() + From fb5fc55fb1b09ca180663c190a3fce49cac53036 Mon Sep 17 00:00:00 2001 From: Kelsey Bisson <48059682+kelseybisson@users.noreply.github.com> Date: Wed, 23 Feb 2022 09:39:34 -0700 Subject: [PATCH 002/124] Create get_argo.py Download the 'classic' argo data with physical variables only --- get_argo.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 get_argo.py diff --git a/get_argo.py b/get_argo.py new file mode 100644 index 000000000..4e4db6049 --- /dev/null +++ b/get_argo.py @@ -0,0 +1,55 @@ +import requests +import pandas as pd +import os + + +##### +# Get current directory to save file into + +curDir = os.getcwd() + + +# Get a selected region from Argovis + +def get_selection_profiles(startDate, endDate, shape, presRange=None): + baseURL = 'https://argovis.colorado.edu/selection/profiles/' + startDateQuery = '?startDate=' + startDate + endDateQuery = '&endDate=' + endDate + shapeQuery = '&shape='+shape + if not presRange == None: + pressRangeQuery = '&presRange;=' + presRange + url = baseURL + startDateQuery + endDateQuery + pressRangeQuery + shapeQuery + else: + url = baseURL + startDateQuery + endDateQuery + shapeQuery + resp = requests.get(url) + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + return "Error: Unexpected response {}".format(resp) + selectionProfiles = resp.json() + return selectionProfiles + +## Get platform information +def parse_into_df(profiles): + #initialize dict + meas_keys = profiles[0]['measurements'][0].keys() + df = pd.DataFrame(columns=meas_keys) + for profile in profiles: + profileDf = pd.DataFrame(profile['measurements']) + profileDf['cycle_number'] = profile['cycle_number'] + profileDf['profile_id'] = profile['_id'] + profileDf['lat'] = profile['lat'] + profileDf['lon'] = profile['lon'] + profileDf['date'] = profile['date'] + df = pd.concat([df, profileDf], sort=False) + return df + +# set start date, end date, lat/lon coordinates for the shape of region and pres range + +startDate='2017-9-15' +endDate='2017-10-31' +# shape should be nested array with lon, lat coords. +shape = '[[[-18.6,31.7],[-18.6,37.7],[-5.9,37.7],[-5.9,31.7],[-18.6,31.7]]]' +presRange='[0,30]' +selectionProfiles = get_selection_profiles(startDate, endDate, shape, presRange) +if len(selectionProfiles) > 0: + selectionDf = parse_into_df(selectionProfiles) From 715440b45ba87922f74c51ed9eceadd568e182d4 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 28 Feb 2022 11:15:33 -0500 Subject: [PATCH 003/124] begin implementing argo dataset --- icepyx/quest/dataset_scripts/argo.py | 8 ++++++++ icepyx/quest/dataset_scripts/dataset.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 icepyx/quest/dataset_scripts/argo.py diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py new file mode 100644 index 000000000..5e2251f04 --- /dev/null +++ b/icepyx/quest/dataset_scripts/argo.py @@ -0,0 +1,8 @@ +from dataset import DataSet + +class Argo(DataSet): + + def __init__(self, boundingbox, timeframe): + super.__init__(boundingbox, timeframe) + + self.startdate = self.time_frame[] diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index 13e926229..b685ddf7c 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -1,9 +1,10 @@ import warnings +from ../../core/query import GenQuery warnings.filterwarnings("ignore") -class DataSet: +class DataSet(GenQuery): """ Parent Class for all supported datasets (i.e. ATL03, ATL07, MODIS, etc.) From d2260d6a326b95b72f5619907f3aeab397ea33e1 Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 8 Mar 2022 14:08:14 -0500 Subject: [PATCH 004/124] 1st draft implementing argo dataset --- icepyx/quest/dataset_scripts/argo.py | 44 ++++++++++++++++++++++++- icepyx/quest/dataset_scripts/dataset.py | 9 ++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 5e2251f04..33f532da9 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -1,8 +1,50 @@ from dataset import DataSet +import requests +import pandas as pd +import os class Argo(DataSet): def __init__(self, boundingbox, timeframe): super.__init__(boundingbox, timeframe) + self.profiles = None - self.startdate = self.time_frame[] + + def search_data(self, presRange=None): + """ + query dataset given the spatio temporal criteria + and other params specic to the dataset + """ + + # todo: these need to be formatted to satisfy query + baseURL = 'https://argovis.colorado.edu/selection/profiles/' + startDateQuery = '?startDate=' + self._start + endDateQuery = '&endDate=' + self._end + shapeQuery = '&shape=' + self._spat_extent + + if not presRange == None: + pressRangeQuery = '&presRange;=' + presRange + url = baseURL + startDateQuery + endDateQuery + pressRangeQuery + shapeQuery + else: + url = baseURL + startDateQuery + endDateQuery + shapeQuery + resp = requests.get(url) + + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + return "Error: Unexpected response {}".format(resp) + selectionProfiles = resp.json() + self.profiles = self.parse_into_df(selectionProfiles) + + def parse_into_df(self, profiles): + # initialize dict + meas_keys = profiles[0]['measurements'][0].keys() + df = pd.DataFrame(columns=meas_keys) + for profile in profiles: + profileDf = pd.DataFrame(profile['measurements']) + profileDf['cycle_number'] = profile['cycle_number'] + profileDf['profile_id'] = profile['_id'] + profileDf['lat'] = profile['lat'] + profileDf['lon'] = profile['lon'] + profileDf['date'] = profile['date'] + df = pd.concat([df, profileDf], sort=False) + self.profiles = df diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index b685ddf7c..804273ca7 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -12,13 +12,14 @@ class DataSet(GenQuery): colocated data class """ - def __init__(self, boundingbox, timeframe): + def __init__(self, spatial_extent=None, date_range=None, start_time=None, end_time=None): """ * use existing Icepyx functionality to initialise this :param timeframe: datetime """ - self.bounding_box = boundingbox - self.time_frame = timeframe + super().__init__(spatial_extent, date_range, start_time, end_time) + # self.bounding_box = boundingbox + # self.time_frame = timeframe def _fmt_coordinates(self): # use icepyx geospatial module (icepyx core) @@ -38,7 +39,7 @@ def _validate_input(self): """ raise NotImplementedError - def search_data(self, delta_t): + def search_data(self): """ query dataset given the spatio temporal criteria and other params specic to the dataset From 71cfedcae367a6bc2f4f83cb39c44ef3ce42686c Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 26 Apr 2022 16:54:27 -0400 Subject: [PATCH 005/124] implement search_data for physical argo --- icepyx/quest/dataset_scripts/argo.py | 49 +++++++++++++++++++++---- icepyx/quest/dataset_scripts/dataset.py | 2 +- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 33f532da9..976a3048f 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -1,12 +1,14 @@ -from dataset import DataSet +from icepyx.quest.dataset_scripts.dataset import DataSet +from icepyx.core.geospatial import geodataframe import requests import pandas as pd import os +import numpy as np class Argo(DataSet): def __init__(self, boundingbox, timeframe): - super.__init__(boundingbox, timeframe) + super().__init__(boundingbox, timeframe) self.profiles = None @@ -18,24 +20,47 @@ def search_data(self, presRange=None): # todo: these need to be formatted to satisfy query baseURL = 'https://argovis.colorado.edu/selection/profiles/' - startDateQuery = '?startDate=' + self._start - endDateQuery = '&endDate=' + self._end - shapeQuery = '&shape=' + self._spat_extent + startDateQuery = '?startDate=' + self._start.strftime('%Y-%m-%d') + endDateQuery = '&endDate=' + self._end.strftime('%Y-%m-%d') + shapeQuery = '&shape=' + self._fmt_coordinates() if not presRange == None: pressRangeQuery = '&presRange;=' + presRange url = baseURL + startDateQuery + endDateQuery + pressRangeQuery + shapeQuery else: url = baseURL + startDateQuery + endDateQuery + shapeQuery - resp = requests.get(url) + + payload = {'startDate': self._start.strftime('%Y-%m-%d'), + 'endDate': self._end.strftime('%Y-%m-%d'), + 'shape': [self._fmt_coordinates()]} + resp = requests.get(baseURL, params=payload) + print(resp.url) + + # resp = requests.get(url) # Consider any status other than 2xx an error if not resp.status_code // 100 == 2: return "Error: Unexpected response {}".format(resp) selectionProfiles = resp.json() - self.profiles = self.parse_into_df(selectionProfiles) + self.profiles = self._parse_into_df(selectionProfiles) + + def _fmt_coordinates(self): + # todo: make this more robust but for now it works + gdf = geodataframe(self.extent_type, self._spat_extent) + coordinates_array = np.asarray(gdf.geometry[0].exterior.coords) + x = '' + for i in coordinates_array: + coord = '[{0},{1}]'.format(i[0], i[1]) + if x == '': + x = coord + else: + x += ','+coord - def parse_into_df(self, profiles): + x = '[['+ x + ']]' + return x + + + def _parse_into_df(self, profiles): # initialize dict meas_keys = profiles[0]['measurements'][0].keys() df = pd.DataFrame(columns=meas_keys) @@ -48,3 +73,11 @@ def parse_into_df(self, profiles): profileDf['date'] = profile['date'] df = pd.concat([df, profileDf], sort=False) self.profiles = df + +# this is just for the purpose of debugging and should be removed later +if __name__ == '__main__': + # no search results + # reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) + # profiles available + reg_a = Argo([-154, 30,-143, 37], ['2022-04-12', '2022-04-26']) + reg_a.search_data() diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index 804273ca7..c3e053bac 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -1,5 +1,5 @@ import warnings -from ../../core/query import GenQuery +from icepyx.core.query import GenQuery warnings.filterwarnings("ignore") From ec564fa649c41731bf0077d0f9f19a89ed3238ab Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 13 Jun 2022 13:04:56 -0400 Subject: [PATCH 006/124] doctests and general cleanup for physical argo query --- icepyx/quest/dataset_scripts/argo.py | 81 +++++++++++++++++++++------- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 976a3048f..51be89ce4 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -6,46 +6,85 @@ import numpy as np class Argo(DataSet): + """ + Initialises an Argo Dataset object + Used to query physical Argo profiles + -> biogeochemical Argo (BGC) not included + Examples + -------- + # example with profiles available + >>> reg_a = Argo([-154, 30,-143, 37], ['2022-04-12', '2022-04-26']) + >>> reg_a.search_data() + >>> print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) + pres temp lat lon + 0 3.9 18.608 33.401 -153.913 + 1 5.7 18.598 33.401 -153.913 + 2 7.7 18.588 33.401 -153.913 + 3 9.7 18.462 33.401 -153.913 + 4 11.7 18.378 33.401 -153.913 + + # example with no profiles + >>> reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) + >>> reg_a.search_data() + Warning: Query returned no profiles + Please try different search parameters + + + See Also + -------- + DataSet + GenQuery + """ def __init__(self, boundingbox, timeframe): super().__init__(boundingbox, timeframe) self.profiles = None - def search_data(self, presRange=None): + def search_data(self, presRange=None, printURL=False): """ query dataset given the spatio temporal criteria - and other params specic to the dataset + and other params specific to the dataset """ - # todo: these need to be formatted to satisfy query + # builds URL to be submitted baseURL = 'https://argovis.colorado.edu/selection/profiles/' - startDateQuery = '?startDate=' + self._start.strftime('%Y-%m-%d') - endDateQuery = '&endDate=' + self._end.strftime('%Y-%m-%d') - shapeQuery = '&shape=' + self._fmt_coordinates() - - if not presRange == None: - pressRangeQuery = '&presRange;=' + presRange - url = baseURL + startDateQuery + endDateQuery + pressRangeQuery + shapeQuery - else: - url = baseURL + startDateQuery + endDateQuery + shapeQuery - payload = {'startDate': self._start.strftime('%Y-%m-%d'), 'endDate': self._end.strftime('%Y-%m-%d'), 'shape': [self._fmt_coordinates()]} + if presRange: + payload['presRange'] = presRange + + # submit request resp = requests.get(baseURL, params=payload) - print(resp.url) - # resp = requests.get(url) + if printURL: + print(resp.url) # Consider any status other than 2xx an error if not resp.status_code // 100 == 2: - return "Error: Unexpected response {}".format(resp) + msg = "Error: Unexpected response {}".format(resp) + print(msg) + return + selectionProfiles = resp.json() - self.profiles = self._parse_into_df(selectionProfiles) + + # check for the existence of profiles from query + if selectionProfiles == []: + msg = 'Warning: Query returned no profiles\n' \ + 'Please try different search parameters' + print(msg) + return + + # if profiles are found, save them to self as dataframe + self._parse_into_df(selectionProfiles) def _fmt_coordinates(self): - # todo: make this more robust but for now it works + """ + Convert spatial extent into format needed by argovis + i.e. list of polygon coords [[[lat1,lon1],[lat2,lon2],...]] + """ + gdf = geodataframe(self.extent_type, self._spat_extent) coordinates_array = np.asarray(gdf.geometry[0].exterior.coords) x = '' @@ -61,6 +100,11 @@ def _fmt_coordinates(self): def _parse_into_df(self, profiles): + """ + Stores profiles returned by query into dataframe + saves profiles back to self.profiles + returns None + """ # initialize dict meas_keys = profiles[0]['measurements'][0].keys() df = pd.DataFrame(columns=meas_keys) @@ -81,3 +125,4 @@ def _parse_into_df(self, profiles): # profiles available reg_a = Argo([-154, 30,-143, 37], ['2022-04-12', '2022-04-26']) reg_a.search_data() + print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) From 4fb597468695b42f387c257b9b038c68a371f6c8 Mon Sep 17 00:00:00 2001 From: Romina Date: Thu, 23 Jun 2022 13:57:05 -0400 Subject: [PATCH 007/124] beginning of BGC Argo download --- icepyx/quest/dataset_scripts/BGCargo.py | 60 +++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 icepyx/quest/dataset_scripts/BGCargo.py diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py new file mode 100644 index 000000000..30637c486 --- /dev/null +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -0,0 +1,60 @@ +from icepyx.quest.dataset_scripts.dataset import DataSet +from icepyx.quest.dataset_scripts.argo import Argo +from icepyx.core.geospatial import geodataframe +import requests +import pandas as pd +import os +import numpy as np + + +class BGC_Argo(Argo): + def __init__(self, boundingbox, timeframe): + super().__init__(boundingbox, timeframe) + # self.profiles = None + + + def search_data(self, params, presRange=None, printURL=False): + # todo: this currently assumes user specifies exactly two BGC search + # params. Need to iterate should the user provide more than 2, and + # accommodate if user supplies only 1 param + # builds URL to be submitted + baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection/' + payload = {'startDate': self._start.strftime('%Y-%m-%d'), + 'endDate': self._end.strftime('%Y-%m-%d'), + 'shape': [self._fmt_coordinates()], + 'meas_1':params[0], + 'meas_2':params[1]} + + if presRange: + payload['presRange'] = presRange + + # submit request + resp = requests.get(baseURL, params=payload) + + if printURL: + print(resp.url) + + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + msg = "Error: Unexpected response {}".format(resp) + print(msg) + return + + selectionProfiles = resp.json() + + # check for the existence of profiles from query + if selectionProfiles == []: + msg = 'Warning: Query returned no profiles\n' \ + 'Please try different search parameters' + print(msg) + return + + # if profiles are found, save them to self as dataframe + self._parse_into_df(selectionProfiles) + + +if __name__ == '__main__': + reg_a = BGC_Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) + reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) + reg_a.search_data(['doxy', 'pres'], printURL=True) + print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) \ No newline at end of file From 3bd2739d6a9d49b544c150ce09e396b91653df36 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 27 Jun 2022 11:58:13 -0400 Subject: [PATCH 008/124] parse BGC profiles into DF --- icepyx/quest/dataset_scripts/BGCargo.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 30637c486..16f0509c5 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -52,9 +52,29 @@ def search_data(self, params, presRange=None, printURL=False): # if profiles are found, save them to self as dataframe self._parse_into_df(selectionProfiles) + def _parse_into_df(self, profiles): + """ + Stores profiles returned by query into dataframe + saves profiles back to self.profiles + returns None + """ + # initialize dict + meas_keys = profiles[0]['bgcMeas'][0].keys() + df = pd.DataFrame(columns=meas_keys) + for profile in profiles: + profileDf = pd.DataFrame(profile['bgcMeas']) + profileDf['cycle_number'] = profile['cycle_number'] + profileDf['profile_id'] = profile['_id'] + profileDf['lat'] = profile['lat'] + profileDf['lon'] = profile['lon'] + profileDf['date'] = profile['date'] + df = pd.concat([df, profileDf], sort=False) + self.profiles = df if __name__ == '__main__': - reg_a = BGC_Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) + # no profiles available + # reg_a = BGC_Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) + # 24 profiles available reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) reg_a.search_data(['doxy', 'pres'], printURL=True) print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) \ No newline at end of file From 06835d5fe6877cb505d4c2291c0ee3ffaeb41a3c Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 29 Aug 2022 12:28:37 -0400 Subject: [PATCH 009/124] plan to query BGC profiles --- icepyx/quest/dataset_scripts/BGCargo.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 16f0509c5..f83be9144 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -12,11 +12,20 @@ def __init__(self, boundingbox, timeframe): super().__init__(boundingbox, timeframe) # self.profiles = None + def _search_data_BGC_helper(self): + ''' + make request with two params, and identify profiles that contain + remaining params + i.e. mandates the intersection of all specified params + ''' + pass def search_data(self, params, presRange=None, printURL=False): # todo: this currently assumes user specifies exactly two BGC search # params. Need to iterate should the user provide more than 2, and # accommodate if user supplies only 1 param + + # todo: validate list of user-entered params # builds URL to be submitted baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection/' payload = {'startDate': self._start.strftime('%Y-%m-%d'), @@ -52,6 +61,12 @@ def search_data(self, params, presRange=None, printURL=False): # if profiles are found, save them to self as dataframe self._parse_into_df(selectionProfiles) + def validate_parameters(self, params): + 'https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_' + pass + + + def _parse_into_df(self, profiles): """ Stores profiles returned by query into dataframe From dac11de1bb88a3e0a143b1f4871d461a9fada309 Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 6 Sep 2022 12:00:00 -0400 Subject: [PATCH 010/124] validate BGC param input function --- icepyx/quest/dataset_scripts/BGCargo.py | 45 ++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index f83be9144..6356bda9a 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -28,6 +28,9 @@ def search_data(self, params, presRange=None, printURL=False): # todo: validate list of user-entered params # builds URL to be submitted baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection/' + + # todo: identify which 2 params we specify (ignore physical params) + # todo: special case if only 1 BGC measurment is specified payload = {'startDate': self._start.strftime('%Y-%m-%d'), 'endDate': self._end.strftime('%Y-%m-%d'), 'shape': [self._fmt_coordinates()], @@ -58,12 +61,37 @@ def search_data(self, params, presRange=None, printURL=False): print(msg) return + # todo: if additional BGC params (>2 specified), filter results + # if profiles are found, save them to self as dataframe self._parse_into_df(selectionProfiles) - def validate_parameters(self, params): - 'https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_' - pass + def _validate_parameters(self, params): + ''' + Asserts that user-specified parameters are valid as per the Argovis documentation here: + https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_ + ''' + + + valid_params = [ + 'pres', + 'temp', + 'psal', + 'cndx', + 'doxy', + 'chla', + 'cdom', + 'nitrate', + 'bbp700', + 'down_irradiance412', + 'down_irradiance442', + 'down_irradiance490', + 'downwelling_par', + ] + + for i in params: + assert i in valid_params, \ + "Parameter '{0}' is not valid. Valid parameters are {1}".format(i, valid_params) @@ -73,6 +101,7 @@ def _parse_into_df(self, profiles): saves profiles back to self.profiles returns None """ + # todo: check that this makes appropriate BGC cols in the DF # initialize dict meas_keys = profiles[0]['bgcMeas'][0].keys() df = pd.DataFrame(columns=meas_keys) @@ -90,6 +119,12 @@ def _parse_into_df(self, profiles): # no profiles available # reg_a = BGC_Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) # 24 profiles available + reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) - reg_a.search_data(['doxy', 'pres'], printURL=True) - print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) \ No newline at end of file + # reg_a.search_data(['doxy', 'pres'], printURL=True) + # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) + + reg_a._validate_parameters(['doxy', + 'chla', + 'cdomm',]) + From dd47dc56ddce6b0210af0e59a69b6728a7d4b39e Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 12 Sep 2022 11:57:18 -0400 Subject: [PATCH 011/124] order BGC params in order in which they should be queried --- icepyx/quest/dataset_scripts/BGCargo.py | 56 +++++++++++++++---------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 6356bda9a..89547c90e 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -70,29 +70,38 @@ def _validate_parameters(self, params): ''' Asserts that user-specified parameters are valid as per the Argovis documentation here: https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_ - ''' - - valid_params = [ - 'pres', - 'temp', - 'psal', - 'cndx', - 'doxy', - 'chla', - 'cdom', - 'nitrate', - 'bbp700', - 'down_irradiance412', - 'down_irradiance442', - 'down_irradiance490', - 'downwelling_par', - ] + Returns + ------- + the list of params sorted in the order in which they should be queried (least + commonly available to most commonly available) + ''' + # valid params ordered by how commonly they are measured (approx) + valid_params = { + 'pres':0, + 'temp':1, + 'psal':2, + 'cndx':3, + 'doxy':4, + 'chla':5, + 'cdom':6, + 'nitrate':7, + 'bbp700':8, + 'down_irradiance412':9, + 'down_irradiance442':10, + 'down_irradiance490':11, + 'downwelling_par':12, + } + + # checks that params are valid for i in params: - assert i in valid_params, \ - "Parameter '{0}' is not valid. Valid parameters are {1}".format(i, valid_params) + assert i in valid_params.keys(), \ + "Parameter '{0}' is not valid. Valid parameters are {1}".format(i, valid_params.keys()) + # sorts params into order in which they should be queried + params = sorted(params, key= lambda i: valid_params[i], reverse=True) + return params def _parse_into_df(self, profiles): @@ -124,7 +133,10 @@ def _parse_into_df(self, profiles): # reg_a.search_data(['doxy', 'pres'], printURL=True) # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) - reg_a._validate_parameters(['doxy', - 'chla', - 'cdomm',]) + # reg_a._validate_parameters(['doxy', + # 'chla', + # 'cdomm',]) + + p = reg_a._validate_parameters(['nitrate', 'pres', 'doxy']) + print(p) From 88722a13e229e1c29ccd520b26ee40cd6d56a5e0 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 19 Sep 2022 18:25:26 -0400 Subject: [PATCH 012/124] fix bug in parse_into_df() - init blank df to take in union of params from all profiles --- icepyx/quest/dataset_scripts/BGCargo.py | 20 +++++++++++++------- icepyx/quest/dataset_scripts/argo.py | 3 +-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 89547c90e..057b63738 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -25,7 +25,12 @@ def search_data(self, params, presRange=None, printURL=False): # params. Need to iterate should the user provide more than 2, and # accommodate if user supplies only 1 param - # todo: validate list of user-entered params + assert len(params) != 0, 'One or more BGC measurements must be specified.' + + # validate list of user-entered params, sorts into order to be queried + params = self._validate_parameters(params) + + # builds URL to be submitted baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection/' @@ -112,10 +117,11 @@ def _parse_into_df(self, profiles): """ # todo: check that this makes appropriate BGC cols in the DF # initialize dict - meas_keys = profiles[0]['bgcMeas'][0].keys() - df = pd.DataFrame(columns=meas_keys) + # meas_keys = profiles[0]['bgcMeasKeys'] + # df = pd.DataFrame(columns=meas_keys) + df = pd.DataFrame() for profile in profiles: - profileDf = pd.DataFrame(profile['bgcMeas']) + profileDf = pd.DataFrame(profile['bgcMeasKeys']) profileDf['cycle_number'] = profile['cycle_number'] profileDf['profile_id'] = profile['_id'] profileDf['lat'] = profile['lat'] @@ -130,7 +136,7 @@ def _parse_into_df(self, profiles): # 24 profiles available reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) - # reg_a.search_data(['doxy', 'pres'], printURL=True) + reg_a.search_data(['doxy', 'nitrate'], printURL=True) # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) # reg_a._validate_parameters(['doxy', @@ -138,5 +144,5 @@ def _parse_into_df(self, profiles): # 'cdomm',]) - p = reg_a._validate_parameters(['nitrate', 'pres', 'doxy']) - print(p) + # p = reg_a._validate_parameters(['nitrate', 'pres', 'doxy']) + # print(p) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 51be89ce4..72607d395 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -106,8 +106,7 @@ def _parse_into_df(self, profiles): returns None """ # initialize dict - meas_keys = profiles[0]['measurements'][0].keys() - df = pd.DataFrame(columns=meas_keys) + df = pd.DataFrame() for profile in profiles: profileDf = pd.DataFrame(profile['measurements']) profileDf['cycle_number'] = profile['cycle_number'] From bf3cd705869fb7c4d771013e4e3354858d7ef446 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 3 Oct 2022 12:01:12 -0400 Subject: [PATCH 013/124] identify profiles from initial API request containing all required params --- icepyx/quest/dataset_scripts/BGCargo.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 057b63738..de29d9334 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -68,6 +68,9 @@ def search_data(self, params, presRange=None, printURL=False): # todo: if additional BGC params (>2 specified), filter results + + self._filter_profiles(selectionProfiles, params) + # if profiles are found, save them to self as dataframe self._parse_into_df(selectionProfiles) @@ -108,6 +111,19 @@ def _validate_parameters(self, params): params = sorted(params, key= lambda i: valid_params[i], reverse=True) return params + def _filter_profiles(self, profiles, params): + ''' + from a dictionary of all profiles returned by first API request, remove the + profiles that do not contain ALL BGC measurements specified by user + ''' + # todo: filter out BGC profiles + + for i in profiles: + bgc_meas = i['bgcMeasKeys'] + check = all(item in bgc_meas for item in params) + if check: + print(i['_id']) + def _parse_into_df(self, profiles): """ From fcb2422fa4e05c0afd054c819be3a5addc414c79 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 24 Oct 2022 11:57:36 -0400 Subject: [PATCH 014/124] creates df with only profiles that contain all user specified params Need to dload additional params --- icepyx/quest/dataset_scripts/BGCargo.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index de29d9334..058d2ee60 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -27,6 +27,9 @@ def search_data(self, params, presRange=None, printURL=False): assert len(params) != 0, 'One or more BGC measurements must be specified.' + if not 'pres' in params: + params.append('pres') + # validate list of user-entered params, sorts into order to be queried params = self._validate_parameters(params) @@ -74,6 +77,12 @@ def search_data(self, params, presRange=None, printURL=False): # if profiles are found, save them to self as dataframe self._parse_into_df(selectionProfiles) + # todo: download additional params + ''' + make api request by profile to download additional params + then append the necessary cols to the df + ''' + def _validate_parameters(self, params): ''' Asserts that user-specified parameters are valid as per the Argovis documentation here: @@ -117,13 +126,17 @@ def _filter_profiles(self, profiles, params): profiles that do not contain ALL BGC measurements specified by user ''' # todo: filter out BGC profiles - + good_profs = [] for i in profiles: bgc_meas = i['bgcMeasKeys'] check = all(item in bgc_meas for item in params) if check: + good_profs.append(i) print(i['_id']) + profiles = good_profs + print() + def _parse_into_df(self, profiles): """ @@ -137,7 +150,7 @@ def _parse_into_df(self, profiles): # df = pd.DataFrame(columns=meas_keys) df = pd.DataFrame() for profile in profiles: - profileDf = pd.DataFrame(profile['bgcMeasKeys']) + profileDf = pd.DataFrame(profile['bgcMeas']) profileDf['cycle_number'] = profile['cycle_number'] profileDf['profile_id'] = profile['_id'] profileDf['lat'] = profile['lat'] @@ -152,7 +165,7 @@ def _parse_into_df(self, profiles): # 24 profiles available reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) - reg_a.search_data(['doxy', 'nitrate'], printURL=True) + reg_a.search_data(['doxy', 'nitrate', 'down_irradiance412'], printURL=True) # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) # reg_a._validate_parameters(['doxy', From eb9c8ae57eec07976741608a80aca47bf115ac73 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 21 Nov 2022 13:09:47 -0500 Subject: [PATCH 015/124] modified to populate prof df by querying individual profiles --- icepyx/quest/dataset_scripts/BGCargo.py | 97 +++++++++++++++++-------- 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 058d2ee60..2a00e516c 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -20,15 +20,15 @@ def _search_data_BGC_helper(self): ''' pass - def search_data(self, params, presRange=None, printURL=False): + def search_data(self, params, presRange=None, printURL=False, keep_all=True): # todo: this currently assumes user specifies exactly two BGC search # params. Need to iterate should the user provide more than 2, and # accommodate if user supplies only 1 param assert len(params) != 0, 'One or more BGC measurements must be specified.' - if not 'pres' in params: - params.append('pres') + # if not 'pres' in params: + # params.append('pres') # validate list of user-entered params, sorts into order to be queried params = self._validate_parameters(params) @@ -69,19 +69,43 @@ def search_data(self, params, presRange=None, printURL=False): print(msg) return - # todo: if additional BGC params (>2 specified), filter results + prof_ids = self._filter_profiles(selectionProfiles, params) - self._filter_profiles(selectionProfiles, params) + print('{0} valid profiles have been identified'.format(len(prof_ids))) + # iterate and download profiles individually + for i in prof_ids: + print("processing profile", i) + self.download_by_profile(i) - # if profiles are found, save them to self as dataframe - self._parse_into_df(selectionProfiles) + self.profiles.reset_index(inplace=True) - # todo: download additional params + if not keep_all: + # todo: drop BGC measurement columns not specified by user + pass + + def _valid_BGC_params(self): ''' - make api request by profile to download additional params - then append the necessary cols to the df + This is a list of valid BGC params, stored here to remove redundancy + They are ordered by how commonly they are measured (approx) ''' + params = valid_params = { + 'pres':0, + 'temp':1, + 'psal':2, + 'cndx':3, + 'doxy':4, + 'ph_in_situ_total':5, + 'chla':6, + 'cdom':7, + 'nitrate':8, + 'bbp700':9, + 'down_irradiance412':10, + 'down_irradiance442':11, + 'down_irradiance490':12, + 'downwelling_par':13, + } + return params def _validate_parameters(self, params): ''' @@ -95,21 +119,7 @@ def _validate_parameters(self, params): ''' # valid params ordered by how commonly they are measured (approx) - valid_params = { - 'pres':0, - 'temp':1, - 'psal':2, - 'cndx':3, - 'doxy':4, - 'chla':5, - 'cdom':6, - 'nitrate':7, - 'bbp700':8, - 'down_irradiance412':9, - 'down_irradiance442':10, - 'down_irradiance490':11, - 'downwelling_par':12, - } + valid_params = self._valid_BGC_params() # checks that params are valid for i in params: @@ -124,6 +134,7 @@ def _filter_profiles(self, profiles, params): ''' from a dictionary of all profiles returned by first API request, remove the profiles that do not contain ALL BGC measurements specified by user + returns a list of profile ID's that contain all necessary BGC params ''' # todo: filter out BGC profiles good_profs = [] @@ -131,12 +142,21 @@ def _filter_profiles(self, profiles, params): bgc_meas = i['bgcMeasKeys'] check = all(item in bgc_meas for item in params) if check: - good_profs.append(i) - print(i['_id']) + good_profs.append(i['_id']) + # print(i['_id']) - profiles = good_profs - print() + # profiles = good_profs + return good_profs + def download_by_profile(self, profile_number): + url = 'https://argovis.colorado.edu/catalog/profiles/{}'.format(profile_number) + resp = requests.get(url) + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + return "Error: Unexpected response {}".format(resp) + profile = resp.json() + self._parse_into_df(profile) + return profile def _parse_into_df(self, profiles): """ @@ -148,7 +168,16 @@ def _parse_into_df(self, profiles): # initialize dict # meas_keys = profiles[0]['bgcMeasKeys'] # df = pd.DataFrame(columns=meas_keys) - df = pd.DataFrame() + + if not isinstance(profiles, list): + profiles = [profiles] + + # initialise the df (empty or containing previously processed profiles) + if not self.profiles is None: + df = self.profiles + else: + df = pd.DataFrame() + for profile in profiles: profileDf = pd.DataFrame(profile['bgcMeas']) profileDf['cycle_number'] = profile['cycle_number'] @@ -157,6 +186,10 @@ def _parse_into_df(self, profiles): profileDf['lon'] = profile['lon'] profileDf['date'] = profile['date'] df = pd.concat([df, profileDf], sort=False) + # if self.profiles is None: + # df = pd.concat([df, profileDf], sort=False) + # else: + # df = df.merge(profileDf, on='profile_id') self.profiles = df if __name__ == '__main__': @@ -165,9 +198,11 @@ def _parse_into_df(self, profiles): # 24 profiles available reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) - reg_a.search_data(['doxy', 'nitrate', 'down_irradiance412'], printURL=True) + reg_a.search_data(['doxy', 'nitrate'], printURL=True) # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) + # reg_a.download_by_profile('4903026_101') + # reg_a._validate_parameters(['doxy', # 'chla', # 'cdomm',]) From 1582f5b9ec75bb6a0943683d752349cf13b225e8 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 28 Nov 2022 12:12:04 -0500 Subject: [PATCH 016/124] finished up BGC argo download! --- icepyx/quest/dataset_scripts/BGCargo.py | 26 ++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 2a00e516c..8dc3f4eb3 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -21,14 +21,12 @@ def _search_data_BGC_helper(self): pass def search_data(self, params, presRange=None, printURL=False, keep_all=True): - # todo: this currently assumes user specifies exactly two BGC search - # params. Need to iterate should the user provide more than 2, and - # accommodate if user supplies only 1 param assert len(params) != 0, 'One or more BGC measurements must be specified.' - # if not 'pres' in params: - # params.append('pres') + # API request requires exactly 2 measurement params, duplicate single of necessary + if len(params) == 1: + params.append(params[0]) # validate list of user-entered params, sorts into order to be queried params = self._validate_parameters(params) @@ -37,8 +35,6 @@ def search_data(self, params, presRange=None, printURL=False, keep_all=True): # builds URL to be submitted baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection/' - # todo: identify which 2 params we specify (ignore physical params) - # todo: special case if only 1 BGC measurment is specified payload = {'startDate': self._start.strftime('%Y-%m-%d'), 'endDate': self._end.strftime('%Y-%m-%d'), 'shape': [self._fmt_coordinates()], @@ -70,6 +66,7 @@ def search_data(self, params, presRange=None, printURL=False, keep_all=True): return + # deterine which profiles contain all specified params prof_ids = self._filter_profiles(selectionProfiles, params) print('{0} valid profiles have been identified'.format(len(prof_ids))) @@ -81,8 +78,13 @@ def search_data(self, params, presRange=None, printURL=False, keep_all=True): self.profiles.reset_index(inplace=True) if not keep_all: - # todo: drop BGC measurement columns not specified by user - pass + # drop BGC measurement columns not specified by user + drop_params = list(set(list(self._valid_BGC_params())[3:]) - set(params)) + qc_params = [] + for i in drop_params: + qc_params.append(i + '_qc') + drop_params += qc_params + self.profiles.drop(columns=drop_params, inplace=True, errors='ignore') def _valid_BGC_params(self): ''' @@ -103,7 +105,8 @@ def _valid_BGC_params(self): 'down_irradiance412':10, 'down_irradiance442':11, 'down_irradiance490':12, - 'downwelling_par':13, + 'down_irradiance380': 13, + 'downwelling_par':14, } return params @@ -198,7 +201,8 @@ def _parse_into_df(self, profiles): # 24 profiles available reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) - reg_a.search_data(['doxy', 'nitrate'], printURL=True) + # reg_a.search_data(['doxy', 'nitrate', 'down_irradiance412'], printURL=True, keep_all=False) + reg_a.search_data(['down_irradiance412'], printURL=True, keep_all=False) # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) # reg_a.download_by_profile('4903026_101') From f55fd61005ee0bae9007d52ae5abbdb912e92582 Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 17 Jan 2023 16:55:58 -0500 Subject: [PATCH 017/124] assert bounding box type in Argo init, begin framework for unit tests --- icepyx/quest/dataset_scripts/argo.py | 1 + icepyx/tests/test_quest_BGCargo.py | 16 ++++++++++++++++ icepyx/tests/test_quest_argo.py | 14 ++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 icepyx/tests/test_quest_BGCargo.py create mode 100644 icepyx/tests/test_quest_argo.py diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 72607d395..3c04c8f56 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -38,6 +38,7 @@ class Argo(DataSet): """ def __init__(self, boundingbox, timeframe): super().__init__(boundingbox, timeframe) + assert self.spatial._ext_type == 'bounding_box' self.profiles = None diff --git a/icepyx/tests/test_quest_BGCargo.py b/icepyx/tests/test_quest_BGCargo.py new file mode 100644 index 000000000..ceadb855f --- /dev/null +++ b/icepyx/tests/test_quest_BGCargo.py @@ -0,0 +1,16 @@ +import icepyx as ipx +import pytest +import warnings + + +def test_available_profiles(): + pass + +def test_no_available_profiles(): + pass + +def test_valid_BGCparams(): + pass + +def test_invalid_BGCparams(): + pass \ No newline at end of file diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py new file mode 100644 index 000000000..a945d7d75 --- /dev/null +++ b/icepyx/tests/test_quest_argo.py @@ -0,0 +1,14 @@ +import icepyx as ipx +import pytest +import warnings + + +def test_available_profiles(): + pass + +def test_no_available_profiles(): + pass + +def test_valid_spatialextent(): + pass + From d6d38725cde784bb592531e54c642d2b1190511c Mon Sep 17 00:00:00 2001 From: Kelsey Bisson <48059682+kelseybisson@users.noreply.github.com> Date: Wed, 23 Feb 2022 09:26:30 -0700 Subject: [PATCH 018/124] Adding argo search and download script --- argo_BGC_class_sandbox | 154 +++++++++++++++++++++++++++++++++++++++++ get_BGC_argo.py | 96 +++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 argo_BGC_class_sandbox create mode 100644 get_BGC_argo.py diff --git a/argo_BGC_class_sandbox b/argo_BGC_class_sandbox new file mode 100644 index 000000000..a0dda63f5 --- /dev/null +++ b/argo_BGC_class_sandbox @@ -0,0 +1,154 @@ +import requests # dependency for icepyx +import pandas as pd # dependency for icepyx? - geopandas +import os +from .dataset import * + +class Argo_bgc(DataSet): + +# Argo data object to search/download (in one function) for BGC Argo data. + +%spatial_extent : list or string +# Spatial extent of interest, provided as a bounding box, list of polygon coordinates, or +# geospatial polygon file. +# Bounding box coordinates should be provided in decimal degrees as +# [lower-left-longitude, lower-left-latitute, upper-right-longitude, upper-right-latitude]. +# Polygon coordinates should be provided as coordinate pairs in decimal degrees as +# [(longitude1, latitude1), (longitude2, latitude2), ... (longitude_n,latitude_n), (longitude1,latitude1)] +# or +# [longitude1, latitude1, longitude2, latitude2, ... longitude_n,latitude_n, longitude1,latitude1]. + +% timeframe: list (2) of start date and end date, in YYYY-MM-DD + +% meas1, meas2 = string listing of argo measurement, e.g., bbp700, chla, temp, psal, doxy + + def __init__(self, shape, timeframe, meas1, meas2, presRange=None): + self.shape = shape + self.bounding_box = shape.extent # call coord standardization method (see icepyx) + self.time_frame = timeframe # call fmt_timerange + self.meas1 = meas1 + self.meas2 = meas2 + self.presrng = presRange + + def download(self, out_path): + baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection' + meas1Query = '?meas_1=' + self.meas1 + meas2Query = '&meas_2=' + self.meas2 + startDateQuery = '&startDate=' + self.time_frame[0].strftime('%Y-%m-%d') + endDateQuery = '&endDate=' + self.time_frame[1].strftime('%Y-%m-%d') + + shapeQuery = '&shape=' + self.shape # might have to process this + if not self.presrng == None: + pressRangeQuery = '&presRange=' + self.presrng + url = baseURL + meas1Query + meas2Query + startDateQuery + endDateQuery + pressRangeQuery + '&bgcOnly=true' + shapeQuery + else: + url = baseURL + meas1Query + meas2Query + startDateQuery + endDateQuery + '&bgcOnly=true' + shapeQuery + resp = requests.get(url) + + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + return "Error: Unexpected response {}".format(resp) + selectionProfiles = resp.json() + + # save selection profiles somewhere + # return selectionProfiles + + # ---------------------------------------------------------------------- + # Properties + + @property + def dataset(self): + """ + Return the short name dataset ID string associated with the query object. + + """ + return self._dset + + @property + def spatial_extent(self): + """ + Return an array showing the spatial extent of the query object. + Spatial extent is returned as an input type (which depends on how + you initially entered your spatial data) followed by the geometry data. + Bounding box data is [lower-left-longitude, lower-left-latitute, upper-right-longitude, upper-right-latitude]. + Polygon data is [[array of longitudes],[array of corresponding latitudes]]. + + """ + + if self.extent_type == "bounding_box": + return ["bounding box", self._spat_extent] + elif self.extent_type == "polygon": + # return ['polygon', self._spat_extent] + # Note: self._spat_extent is a shapely geometry object + return ["polygon", self._spat_extent.exterior.coords.xy] + else: + return ["unknown spatial type", None] + + @property + def dates(self): + """ + Return an array showing the date range of the query object. + Dates are returned as an array containing the start and end datetime objects, inclusive, in that order. + + """ + return [ + self._start.strftime("%Y-%m-%d"), + self._end.strftime("%Y-%m-%d"), + ] # could also use self._start.date() + + @property + def start_time(self): + """ + Return the start time specified for the start date. + NOTE THAT there is no time input for Argo + """ + return self._start.strftime("%H:%M:%S") + + @property + def end_time(self): + """ + Return the end time specified for the end date. + Examples + """ + return self._end.strftime("%H:%M:%S") + +## DO WE NEED AN ORDER VARS CLASS? or the download already puts data into a dataframe .. maybe order vars could +# save data to CSV? +# IF SO, this code may be relevant for it + +tick1 = 0 +tick2 = 0 +for index, value in enumerate(selectionProfiles): + if meas1 not in value['bgcMeasKeys']: + tick1 += 1 + if meas2 not in value['bgcMeasKeys']: + tick2 += 1 +if tick1 == len(selectionProfiles): + print(f'{meas1} not found in selected data') +if tick2 == len(selectionProfiles): + print(f'{meas2} not found in selected data') + +if tick1 < len(selectionProfiles) & tick2 < len(selectionProfiles): + df = json2dataframe(selectionProfiles, measKey='bgcMeas') + +df.head() + +# NEED TO ADD CODE for the visualization here +def visualize_spatial_extent( + self, + ): # additional args, basemap, zoom level, cmap, export + """ + Creates a map displaying the input spatial extent + Examples + -------- + >>> icepyx.query.Query('ATL06','path/spatialfile.shp',['2019-02-22','2019-02-28']) + >>> reg_a.visualize_spatial_extent + [visual map output] + """ + + world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) + f, ax = plt.subplots(1, figsize=(12, 6)) + world.plot(ax=ax, facecolor="lightgray", edgecolor="gray") + geospatial.geodataframe(self.extent_type, self._spat_extent).plot( + ax=ax, color="#FF8C00", alpha=0.7 + ) + plt.show() \ No newline at end of file diff --git a/get_BGC_argo.py b/get_BGC_argo.py new file mode 100644 index 000000000..cc30b986e --- /dev/null +++ b/get_BGC_argo.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu Oct 29 13:50:54 2020 + +@author: bissonk +""" + +# made for Python 3. It may work with Python 2.7, but has not been well tested + +# libraries to call for all python API calls on Argovis + +import requests +import pandas as pd +import os + +##### +# Get current directory to save file into + +curDir = os.getcwd() + +# Get a selected region from Argovis + + +def get_selection_profiles(startDate, endDate, shape, meas1,meas2, presRange=None): + baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection' + meas1Query = '?meas_1=' + meas1 + meas2Query = '&meas_2=' + meas2 + startDateQuery = '&startDate=' + startDate + endDateQuery = '&endDate=' + endDate + shapeQuery = '&shape='+shape + if not presRange == None: + pressRangeQuery = '&presRange=' + presRange + url = baseURL + meas1Query + meas2Query + startDateQuery + endDateQuery + pressRangeQuery + '&bgcOnly=true' + shapeQuery + else: + url = baseURL + meas1Query + meas2Query + startDateQuery + endDateQuery + '&bgcOnly=true' + shapeQuery + resp = requests.get(url) + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + return "Error: Unexpected response {}".format(resp) + selectionProfiles = resp.json() + return selectionProfiles + + +def json2dataframe(selectionProfiles, measKey='measurements'): + """ convert json data to Pandas DataFrame """ + # Make sure we deal with a list + if isinstance(selectionProfiles, list): + data = selectionProfiles + else: + data = [selectionProfiles] + # Transform + rows = [] + for profile in data: + keys = [x for x in profile.keys() if x not in ['measurements', 'bgcMeas']] + meta_row = dict((key, profile[key]) for key in keys) + for row in profile[measKey]: + row.update(meta_row) + rows.append(row) + df = pd.DataFrame(rows) + return df +# set start date, end date, lat/lon coordinates for the shape of region and pres range + +startDate='2020-10-08' +endDate='2020-10-22' +# shape should be nested array with lon, lat coords. +shape = '[[[-49.21875,48.806863],[-55.229808,54.85326],[-63.28125,60.500525],[-60.46875,64.396938],[-49.746094,61.185625],[-38.496094,54.059388],[-41.484375,47.754098],[-49.21875,48.806863]]]' +presRange='[0,30]' +meas1 = 'bbp700' +meas2 = 'chla' + +meas1= 'temp' +meas2='psal' +# tested with meas1 = temp, meas2 = psal and it works + +selectionProfiles = get_selection_profiles(startDate, endDate, shape, meas1, meas2, presRange=None) + + +# loop thru profiles and search for measurement +tick1 = 0 +tick2 = 0 +for index, value in enumerate(selectionProfiles): + if meas1 not in value['bgcMeasKeys']: + tick1 += 1 + if meas2 not in value['bgcMeasKeys']: + tick2 += 1 +if tick1 == len(selectionProfiles): + print(f'{meas1} not found in selected data') +if tick2 == len(selectionProfiles): + print(f'{meas2} not found in selected data') + +if tick1 < len(selectionProfiles) & tick2 < len(selectionProfiles): + df = json2dataframe(selectionProfiles, measKey='bgcMeas') + +df.head() + From 7fc3b79249bdf4d9a81e99b0e0ea7503e0ec1adc Mon Sep 17 00:00:00 2001 From: Kelsey Bisson <48059682+kelseybisson@users.noreply.github.com> Date: Wed, 23 Feb 2022 09:39:34 -0700 Subject: [PATCH 019/124] Create get_argo.py Download the 'classic' argo data with physical variables only --- get_argo.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 get_argo.py diff --git a/get_argo.py b/get_argo.py new file mode 100644 index 000000000..4e4db6049 --- /dev/null +++ b/get_argo.py @@ -0,0 +1,55 @@ +import requests +import pandas as pd +import os + + +##### +# Get current directory to save file into + +curDir = os.getcwd() + + +# Get a selected region from Argovis + +def get_selection_profiles(startDate, endDate, shape, presRange=None): + baseURL = 'https://argovis.colorado.edu/selection/profiles/' + startDateQuery = '?startDate=' + startDate + endDateQuery = '&endDate=' + endDate + shapeQuery = '&shape='+shape + if not presRange == None: + pressRangeQuery = '&presRange;=' + presRange + url = baseURL + startDateQuery + endDateQuery + pressRangeQuery + shapeQuery + else: + url = baseURL + startDateQuery + endDateQuery + shapeQuery + resp = requests.get(url) + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + return "Error: Unexpected response {}".format(resp) + selectionProfiles = resp.json() + return selectionProfiles + +## Get platform information +def parse_into_df(profiles): + #initialize dict + meas_keys = profiles[0]['measurements'][0].keys() + df = pd.DataFrame(columns=meas_keys) + for profile in profiles: + profileDf = pd.DataFrame(profile['measurements']) + profileDf['cycle_number'] = profile['cycle_number'] + profileDf['profile_id'] = profile['_id'] + profileDf['lat'] = profile['lat'] + profileDf['lon'] = profile['lon'] + profileDf['date'] = profile['date'] + df = pd.concat([df, profileDf], sort=False) + return df + +# set start date, end date, lat/lon coordinates for the shape of region and pres range + +startDate='2017-9-15' +endDate='2017-10-31' +# shape should be nested array with lon, lat coords. +shape = '[[[-18.6,31.7],[-18.6,37.7],[-5.9,37.7],[-5.9,31.7],[-18.6,31.7]]]' +presRange='[0,30]' +selectionProfiles = get_selection_profiles(startDate, endDate, shape, presRange) +if len(selectionProfiles) > 0: + selectionDf = parse_into_df(selectionProfiles) From 195a4f180675e7ef58528915e91c49378afec5c3 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 28 Feb 2022 11:15:33 -0500 Subject: [PATCH 020/124] begin implementing argo dataset --- icepyx/quest/dataset_scripts/argo.py | 8 ++++++++ icepyx/quest/dataset_scripts/dataset.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 icepyx/quest/dataset_scripts/argo.py diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py new file mode 100644 index 000000000..5e2251f04 --- /dev/null +++ b/icepyx/quest/dataset_scripts/argo.py @@ -0,0 +1,8 @@ +from dataset import DataSet + +class Argo(DataSet): + + def __init__(self, boundingbox, timeframe): + super.__init__(boundingbox, timeframe) + + self.startdate = self.time_frame[] diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index 13e926229..b685ddf7c 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -1,9 +1,10 @@ import warnings +from ../../core/query import GenQuery warnings.filterwarnings("ignore") -class DataSet: +class DataSet(GenQuery): """ Parent Class for all supported datasets (i.e. ATL03, ATL07, MODIS, etc.) From df344244d1edc5a706a780a95c847934419cbcea Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 8 Mar 2022 14:08:14 -0500 Subject: [PATCH 021/124] 1st draft implementing argo dataset --- icepyx/quest/dataset_scripts/argo.py | 44 ++++++++++++++++++++++++- icepyx/quest/dataset_scripts/dataset.py | 9 ++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 5e2251f04..33f532da9 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -1,8 +1,50 @@ from dataset import DataSet +import requests +import pandas as pd +import os class Argo(DataSet): def __init__(self, boundingbox, timeframe): super.__init__(boundingbox, timeframe) + self.profiles = None - self.startdate = self.time_frame[] + + def search_data(self, presRange=None): + """ + query dataset given the spatio temporal criteria + and other params specic to the dataset + """ + + # todo: these need to be formatted to satisfy query + baseURL = 'https://argovis.colorado.edu/selection/profiles/' + startDateQuery = '?startDate=' + self._start + endDateQuery = '&endDate=' + self._end + shapeQuery = '&shape=' + self._spat_extent + + if not presRange == None: + pressRangeQuery = '&presRange;=' + presRange + url = baseURL + startDateQuery + endDateQuery + pressRangeQuery + shapeQuery + else: + url = baseURL + startDateQuery + endDateQuery + shapeQuery + resp = requests.get(url) + + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + return "Error: Unexpected response {}".format(resp) + selectionProfiles = resp.json() + self.profiles = self.parse_into_df(selectionProfiles) + + def parse_into_df(self, profiles): + # initialize dict + meas_keys = profiles[0]['measurements'][0].keys() + df = pd.DataFrame(columns=meas_keys) + for profile in profiles: + profileDf = pd.DataFrame(profile['measurements']) + profileDf['cycle_number'] = profile['cycle_number'] + profileDf['profile_id'] = profile['_id'] + profileDf['lat'] = profile['lat'] + profileDf['lon'] = profile['lon'] + profileDf['date'] = profile['date'] + df = pd.concat([df, profileDf], sort=False) + self.profiles = df diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index b685ddf7c..804273ca7 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -12,13 +12,14 @@ class DataSet(GenQuery): colocated data class """ - def __init__(self, boundingbox, timeframe): + def __init__(self, spatial_extent=None, date_range=None, start_time=None, end_time=None): """ * use existing Icepyx functionality to initialise this :param timeframe: datetime """ - self.bounding_box = boundingbox - self.time_frame = timeframe + super().__init__(spatial_extent, date_range, start_time, end_time) + # self.bounding_box = boundingbox + # self.time_frame = timeframe def _fmt_coordinates(self): # use icepyx geospatial module (icepyx core) @@ -38,7 +39,7 @@ def _validate_input(self): """ raise NotImplementedError - def search_data(self, delta_t): + def search_data(self): """ query dataset given the spatio temporal criteria and other params specic to the dataset From 390b7a9a418f10d508cfe51303afb3b6bdbed43e Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 26 Apr 2022 16:54:27 -0400 Subject: [PATCH 022/124] implement search_data for physical argo --- icepyx/quest/dataset_scripts/argo.py | 49 +++++++++++++++++++++---- icepyx/quest/dataset_scripts/dataset.py | 2 +- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 33f532da9..976a3048f 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -1,12 +1,14 @@ -from dataset import DataSet +from icepyx.quest.dataset_scripts.dataset import DataSet +from icepyx.core.geospatial import geodataframe import requests import pandas as pd import os +import numpy as np class Argo(DataSet): def __init__(self, boundingbox, timeframe): - super.__init__(boundingbox, timeframe) + super().__init__(boundingbox, timeframe) self.profiles = None @@ -18,24 +20,47 @@ def search_data(self, presRange=None): # todo: these need to be formatted to satisfy query baseURL = 'https://argovis.colorado.edu/selection/profiles/' - startDateQuery = '?startDate=' + self._start - endDateQuery = '&endDate=' + self._end - shapeQuery = '&shape=' + self._spat_extent + startDateQuery = '?startDate=' + self._start.strftime('%Y-%m-%d') + endDateQuery = '&endDate=' + self._end.strftime('%Y-%m-%d') + shapeQuery = '&shape=' + self._fmt_coordinates() if not presRange == None: pressRangeQuery = '&presRange;=' + presRange url = baseURL + startDateQuery + endDateQuery + pressRangeQuery + shapeQuery else: url = baseURL + startDateQuery + endDateQuery + shapeQuery - resp = requests.get(url) + + payload = {'startDate': self._start.strftime('%Y-%m-%d'), + 'endDate': self._end.strftime('%Y-%m-%d'), + 'shape': [self._fmt_coordinates()]} + resp = requests.get(baseURL, params=payload) + print(resp.url) + + # resp = requests.get(url) # Consider any status other than 2xx an error if not resp.status_code // 100 == 2: return "Error: Unexpected response {}".format(resp) selectionProfiles = resp.json() - self.profiles = self.parse_into_df(selectionProfiles) + self.profiles = self._parse_into_df(selectionProfiles) + + def _fmt_coordinates(self): + # todo: make this more robust but for now it works + gdf = geodataframe(self.extent_type, self._spat_extent) + coordinates_array = np.asarray(gdf.geometry[0].exterior.coords) + x = '' + for i in coordinates_array: + coord = '[{0},{1}]'.format(i[0], i[1]) + if x == '': + x = coord + else: + x += ','+coord - def parse_into_df(self, profiles): + x = '[['+ x + ']]' + return x + + + def _parse_into_df(self, profiles): # initialize dict meas_keys = profiles[0]['measurements'][0].keys() df = pd.DataFrame(columns=meas_keys) @@ -48,3 +73,11 @@ def parse_into_df(self, profiles): profileDf['date'] = profile['date'] df = pd.concat([df, profileDf], sort=False) self.profiles = df + +# this is just for the purpose of debugging and should be removed later +if __name__ == '__main__': + # no search results + # reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) + # profiles available + reg_a = Argo([-154, 30,-143, 37], ['2022-04-12', '2022-04-26']) + reg_a.search_data() diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index 804273ca7..c3e053bac 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -1,5 +1,5 @@ import warnings -from ../../core/query import GenQuery +from icepyx.core.query import GenQuery warnings.filterwarnings("ignore") From 6824d27e87793d948ecb5695268e5b594c5ee8f2 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 13 Jun 2022 13:04:56 -0400 Subject: [PATCH 023/124] doctests and general cleanup for physical argo query --- icepyx/quest/dataset_scripts/argo.py | 81 +++++++++++++++++++++------- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 976a3048f..51be89ce4 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -6,46 +6,85 @@ import numpy as np class Argo(DataSet): + """ + Initialises an Argo Dataset object + Used to query physical Argo profiles + -> biogeochemical Argo (BGC) not included + Examples + -------- + # example with profiles available + >>> reg_a = Argo([-154, 30,-143, 37], ['2022-04-12', '2022-04-26']) + >>> reg_a.search_data() + >>> print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) + pres temp lat lon + 0 3.9 18.608 33.401 -153.913 + 1 5.7 18.598 33.401 -153.913 + 2 7.7 18.588 33.401 -153.913 + 3 9.7 18.462 33.401 -153.913 + 4 11.7 18.378 33.401 -153.913 + + # example with no profiles + >>> reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) + >>> reg_a.search_data() + Warning: Query returned no profiles + Please try different search parameters + + + See Also + -------- + DataSet + GenQuery + """ def __init__(self, boundingbox, timeframe): super().__init__(boundingbox, timeframe) self.profiles = None - def search_data(self, presRange=None): + def search_data(self, presRange=None, printURL=False): """ query dataset given the spatio temporal criteria - and other params specic to the dataset + and other params specific to the dataset """ - # todo: these need to be formatted to satisfy query + # builds URL to be submitted baseURL = 'https://argovis.colorado.edu/selection/profiles/' - startDateQuery = '?startDate=' + self._start.strftime('%Y-%m-%d') - endDateQuery = '&endDate=' + self._end.strftime('%Y-%m-%d') - shapeQuery = '&shape=' + self._fmt_coordinates() - - if not presRange == None: - pressRangeQuery = '&presRange;=' + presRange - url = baseURL + startDateQuery + endDateQuery + pressRangeQuery + shapeQuery - else: - url = baseURL + startDateQuery + endDateQuery + shapeQuery - payload = {'startDate': self._start.strftime('%Y-%m-%d'), 'endDate': self._end.strftime('%Y-%m-%d'), 'shape': [self._fmt_coordinates()]} + if presRange: + payload['presRange'] = presRange + + # submit request resp = requests.get(baseURL, params=payload) - print(resp.url) - # resp = requests.get(url) + if printURL: + print(resp.url) # Consider any status other than 2xx an error if not resp.status_code // 100 == 2: - return "Error: Unexpected response {}".format(resp) + msg = "Error: Unexpected response {}".format(resp) + print(msg) + return + selectionProfiles = resp.json() - self.profiles = self._parse_into_df(selectionProfiles) + + # check for the existence of profiles from query + if selectionProfiles == []: + msg = 'Warning: Query returned no profiles\n' \ + 'Please try different search parameters' + print(msg) + return + + # if profiles are found, save them to self as dataframe + self._parse_into_df(selectionProfiles) def _fmt_coordinates(self): - # todo: make this more robust but for now it works + """ + Convert spatial extent into format needed by argovis + i.e. list of polygon coords [[[lat1,lon1],[lat2,lon2],...]] + """ + gdf = geodataframe(self.extent_type, self._spat_extent) coordinates_array = np.asarray(gdf.geometry[0].exterior.coords) x = '' @@ -61,6 +100,11 @@ def _fmt_coordinates(self): def _parse_into_df(self, profiles): + """ + Stores profiles returned by query into dataframe + saves profiles back to self.profiles + returns None + """ # initialize dict meas_keys = profiles[0]['measurements'][0].keys() df = pd.DataFrame(columns=meas_keys) @@ -81,3 +125,4 @@ def _parse_into_df(self, profiles): # profiles available reg_a = Argo([-154, 30,-143, 37], ['2022-04-12', '2022-04-26']) reg_a.search_data() + print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) From 58092f9b4209127cf63bed0362d73d03254589fc Mon Sep 17 00:00:00 2001 From: Romina Date: Thu, 23 Jun 2022 13:57:05 -0400 Subject: [PATCH 024/124] beginning of BGC Argo download --- icepyx/quest/dataset_scripts/BGCargo.py | 60 +++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 icepyx/quest/dataset_scripts/BGCargo.py diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py new file mode 100644 index 000000000..30637c486 --- /dev/null +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -0,0 +1,60 @@ +from icepyx.quest.dataset_scripts.dataset import DataSet +from icepyx.quest.dataset_scripts.argo import Argo +from icepyx.core.geospatial import geodataframe +import requests +import pandas as pd +import os +import numpy as np + + +class BGC_Argo(Argo): + def __init__(self, boundingbox, timeframe): + super().__init__(boundingbox, timeframe) + # self.profiles = None + + + def search_data(self, params, presRange=None, printURL=False): + # todo: this currently assumes user specifies exactly two BGC search + # params. Need to iterate should the user provide more than 2, and + # accommodate if user supplies only 1 param + # builds URL to be submitted + baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection/' + payload = {'startDate': self._start.strftime('%Y-%m-%d'), + 'endDate': self._end.strftime('%Y-%m-%d'), + 'shape': [self._fmt_coordinates()], + 'meas_1':params[0], + 'meas_2':params[1]} + + if presRange: + payload['presRange'] = presRange + + # submit request + resp = requests.get(baseURL, params=payload) + + if printURL: + print(resp.url) + + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + msg = "Error: Unexpected response {}".format(resp) + print(msg) + return + + selectionProfiles = resp.json() + + # check for the existence of profiles from query + if selectionProfiles == []: + msg = 'Warning: Query returned no profiles\n' \ + 'Please try different search parameters' + print(msg) + return + + # if profiles are found, save them to self as dataframe + self._parse_into_df(selectionProfiles) + + +if __name__ == '__main__': + reg_a = BGC_Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) + reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) + reg_a.search_data(['doxy', 'pres'], printURL=True) + print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) \ No newline at end of file From ae486f26685001aac0916baf2641f3f15daa001c Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 27 Jun 2022 11:58:13 -0400 Subject: [PATCH 025/124] parse BGC profiles into DF --- icepyx/quest/dataset_scripts/BGCargo.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 30637c486..16f0509c5 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -52,9 +52,29 @@ def search_data(self, params, presRange=None, printURL=False): # if profiles are found, save them to self as dataframe self._parse_into_df(selectionProfiles) + def _parse_into_df(self, profiles): + """ + Stores profiles returned by query into dataframe + saves profiles back to self.profiles + returns None + """ + # initialize dict + meas_keys = profiles[0]['bgcMeas'][0].keys() + df = pd.DataFrame(columns=meas_keys) + for profile in profiles: + profileDf = pd.DataFrame(profile['bgcMeas']) + profileDf['cycle_number'] = profile['cycle_number'] + profileDf['profile_id'] = profile['_id'] + profileDf['lat'] = profile['lat'] + profileDf['lon'] = profile['lon'] + profileDf['date'] = profile['date'] + df = pd.concat([df, profileDf], sort=False) + self.profiles = df if __name__ == '__main__': - reg_a = BGC_Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) + # no profiles available + # reg_a = BGC_Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) + # 24 profiles available reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) reg_a.search_data(['doxy', 'pres'], printURL=True) print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) \ No newline at end of file From 92f8a0dd6886b2c63a3928a714ced63e817ffcae Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 29 Aug 2022 12:28:37 -0400 Subject: [PATCH 026/124] plan to query BGC profiles --- icepyx/quest/dataset_scripts/BGCargo.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 16f0509c5..f83be9144 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -12,11 +12,20 @@ def __init__(self, boundingbox, timeframe): super().__init__(boundingbox, timeframe) # self.profiles = None + def _search_data_BGC_helper(self): + ''' + make request with two params, and identify profiles that contain + remaining params + i.e. mandates the intersection of all specified params + ''' + pass def search_data(self, params, presRange=None, printURL=False): # todo: this currently assumes user specifies exactly two BGC search # params. Need to iterate should the user provide more than 2, and # accommodate if user supplies only 1 param + + # todo: validate list of user-entered params # builds URL to be submitted baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection/' payload = {'startDate': self._start.strftime('%Y-%m-%d'), @@ -52,6 +61,12 @@ def search_data(self, params, presRange=None, printURL=False): # if profiles are found, save them to self as dataframe self._parse_into_df(selectionProfiles) + def validate_parameters(self, params): + 'https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_' + pass + + + def _parse_into_df(self, profiles): """ Stores profiles returned by query into dataframe From 0285be18f5765f9183e681c772a23bf17b7ad6c7 Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 6 Sep 2022 12:00:00 -0400 Subject: [PATCH 027/124] validate BGC param input function --- icepyx/quest/dataset_scripts/BGCargo.py | 45 ++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index f83be9144..6356bda9a 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -28,6 +28,9 @@ def search_data(self, params, presRange=None, printURL=False): # todo: validate list of user-entered params # builds URL to be submitted baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection/' + + # todo: identify which 2 params we specify (ignore physical params) + # todo: special case if only 1 BGC measurment is specified payload = {'startDate': self._start.strftime('%Y-%m-%d'), 'endDate': self._end.strftime('%Y-%m-%d'), 'shape': [self._fmt_coordinates()], @@ -58,12 +61,37 @@ def search_data(self, params, presRange=None, printURL=False): print(msg) return + # todo: if additional BGC params (>2 specified), filter results + # if profiles are found, save them to self as dataframe self._parse_into_df(selectionProfiles) - def validate_parameters(self, params): - 'https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_' - pass + def _validate_parameters(self, params): + ''' + Asserts that user-specified parameters are valid as per the Argovis documentation here: + https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_ + ''' + + + valid_params = [ + 'pres', + 'temp', + 'psal', + 'cndx', + 'doxy', + 'chla', + 'cdom', + 'nitrate', + 'bbp700', + 'down_irradiance412', + 'down_irradiance442', + 'down_irradiance490', + 'downwelling_par', + ] + + for i in params: + assert i in valid_params, \ + "Parameter '{0}' is not valid. Valid parameters are {1}".format(i, valid_params) @@ -73,6 +101,7 @@ def _parse_into_df(self, profiles): saves profiles back to self.profiles returns None """ + # todo: check that this makes appropriate BGC cols in the DF # initialize dict meas_keys = profiles[0]['bgcMeas'][0].keys() df = pd.DataFrame(columns=meas_keys) @@ -90,6 +119,12 @@ def _parse_into_df(self, profiles): # no profiles available # reg_a = BGC_Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) # 24 profiles available + reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) - reg_a.search_data(['doxy', 'pres'], printURL=True) - print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) \ No newline at end of file + # reg_a.search_data(['doxy', 'pres'], printURL=True) + # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) + + reg_a._validate_parameters(['doxy', + 'chla', + 'cdomm',]) + From 747af3ab750b76942447574671ff6406fd2e6d37 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 12 Sep 2022 11:57:18 -0400 Subject: [PATCH 028/124] order BGC params in order in which they should be queried --- icepyx/quest/dataset_scripts/BGCargo.py | 56 +++++++++++++++---------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 6356bda9a..89547c90e 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -70,29 +70,38 @@ def _validate_parameters(self, params): ''' Asserts that user-specified parameters are valid as per the Argovis documentation here: https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_ - ''' - - valid_params = [ - 'pres', - 'temp', - 'psal', - 'cndx', - 'doxy', - 'chla', - 'cdom', - 'nitrate', - 'bbp700', - 'down_irradiance412', - 'down_irradiance442', - 'down_irradiance490', - 'downwelling_par', - ] + Returns + ------- + the list of params sorted in the order in which they should be queried (least + commonly available to most commonly available) + ''' + # valid params ordered by how commonly they are measured (approx) + valid_params = { + 'pres':0, + 'temp':1, + 'psal':2, + 'cndx':3, + 'doxy':4, + 'chla':5, + 'cdom':6, + 'nitrate':7, + 'bbp700':8, + 'down_irradiance412':9, + 'down_irradiance442':10, + 'down_irradiance490':11, + 'downwelling_par':12, + } + + # checks that params are valid for i in params: - assert i in valid_params, \ - "Parameter '{0}' is not valid. Valid parameters are {1}".format(i, valid_params) + assert i in valid_params.keys(), \ + "Parameter '{0}' is not valid. Valid parameters are {1}".format(i, valid_params.keys()) + # sorts params into order in which they should be queried + params = sorted(params, key= lambda i: valid_params[i], reverse=True) + return params def _parse_into_df(self, profiles): @@ -124,7 +133,10 @@ def _parse_into_df(self, profiles): # reg_a.search_data(['doxy', 'pres'], printURL=True) # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) - reg_a._validate_parameters(['doxy', - 'chla', - 'cdomm',]) + # reg_a._validate_parameters(['doxy', + # 'chla', + # 'cdomm',]) + + p = reg_a._validate_parameters(['nitrate', 'pres', 'doxy']) + print(p) From cf600c64c563e9ae8f16ef10ebea61a5fcd46850 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 19 Sep 2022 18:25:26 -0400 Subject: [PATCH 029/124] fix bug in parse_into_df() - init blank df to take in union of params from all profiles --- icepyx/quest/dataset_scripts/BGCargo.py | 20 +++++++++++++------- icepyx/quest/dataset_scripts/argo.py | 3 +-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 89547c90e..057b63738 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -25,7 +25,12 @@ def search_data(self, params, presRange=None, printURL=False): # params. Need to iterate should the user provide more than 2, and # accommodate if user supplies only 1 param - # todo: validate list of user-entered params + assert len(params) != 0, 'One or more BGC measurements must be specified.' + + # validate list of user-entered params, sorts into order to be queried + params = self._validate_parameters(params) + + # builds URL to be submitted baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection/' @@ -112,10 +117,11 @@ def _parse_into_df(self, profiles): """ # todo: check that this makes appropriate BGC cols in the DF # initialize dict - meas_keys = profiles[0]['bgcMeas'][0].keys() - df = pd.DataFrame(columns=meas_keys) + # meas_keys = profiles[0]['bgcMeasKeys'] + # df = pd.DataFrame(columns=meas_keys) + df = pd.DataFrame() for profile in profiles: - profileDf = pd.DataFrame(profile['bgcMeas']) + profileDf = pd.DataFrame(profile['bgcMeasKeys']) profileDf['cycle_number'] = profile['cycle_number'] profileDf['profile_id'] = profile['_id'] profileDf['lat'] = profile['lat'] @@ -130,7 +136,7 @@ def _parse_into_df(self, profiles): # 24 profiles available reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) - # reg_a.search_data(['doxy', 'pres'], printURL=True) + reg_a.search_data(['doxy', 'nitrate'], printURL=True) # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) # reg_a._validate_parameters(['doxy', @@ -138,5 +144,5 @@ def _parse_into_df(self, profiles): # 'cdomm',]) - p = reg_a._validate_parameters(['nitrate', 'pres', 'doxy']) - print(p) + # p = reg_a._validate_parameters(['nitrate', 'pres', 'doxy']) + # print(p) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 51be89ce4..72607d395 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -106,8 +106,7 @@ def _parse_into_df(self, profiles): returns None """ # initialize dict - meas_keys = profiles[0]['measurements'][0].keys() - df = pd.DataFrame(columns=meas_keys) + df = pd.DataFrame() for profile in profiles: profileDf = pd.DataFrame(profile['measurements']) profileDf['cycle_number'] = profile['cycle_number'] From 29ee8c40b7852757f1c87ee2d2de0295a9c8f4ba Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 3 Oct 2022 12:01:12 -0400 Subject: [PATCH 030/124] identify profiles from initial API request containing all required params --- icepyx/quest/dataset_scripts/BGCargo.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 057b63738..de29d9334 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -68,6 +68,9 @@ def search_data(self, params, presRange=None, printURL=False): # todo: if additional BGC params (>2 specified), filter results + + self._filter_profiles(selectionProfiles, params) + # if profiles are found, save them to self as dataframe self._parse_into_df(selectionProfiles) @@ -108,6 +111,19 @@ def _validate_parameters(self, params): params = sorted(params, key= lambda i: valid_params[i], reverse=True) return params + def _filter_profiles(self, profiles, params): + ''' + from a dictionary of all profiles returned by first API request, remove the + profiles that do not contain ALL BGC measurements specified by user + ''' + # todo: filter out BGC profiles + + for i in profiles: + bgc_meas = i['bgcMeasKeys'] + check = all(item in bgc_meas for item in params) + if check: + print(i['_id']) + def _parse_into_df(self, profiles): """ From 934e1a6f6d8c0e99d4a1024d1f5e6b1bd98ed057 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 24 Oct 2022 11:57:36 -0400 Subject: [PATCH 031/124] creates df with only profiles that contain all user specified params Need to dload additional params --- icepyx/quest/dataset_scripts/BGCargo.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index de29d9334..058d2ee60 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -27,6 +27,9 @@ def search_data(self, params, presRange=None, printURL=False): assert len(params) != 0, 'One or more BGC measurements must be specified.' + if not 'pres' in params: + params.append('pres') + # validate list of user-entered params, sorts into order to be queried params = self._validate_parameters(params) @@ -74,6 +77,12 @@ def search_data(self, params, presRange=None, printURL=False): # if profiles are found, save them to self as dataframe self._parse_into_df(selectionProfiles) + # todo: download additional params + ''' + make api request by profile to download additional params + then append the necessary cols to the df + ''' + def _validate_parameters(self, params): ''' Asserts that user-specified parameters are valid as per the Argovis documentation here: @@ -117,13 +126,17 @@ def _filter_profiles(self, profiles, params): profiles that do not contain ALL BGC measurements specified by user ''' # todo: filter out BGC profiles - + good_profs = [] for i in profiles: bgc_meas = i['bgcMeasKeys'] check = all(item in bgc_meas for item in params) if check: + good_profs.append(i) print(i['_id']) + profiles = good_profs + print() + def _parse_into_df(self, profiles): """ @@ -137,7 +150,7 @@ def _parse_into_df(self, profiles): # df = pd.DataFrame(columns=meas_keys) df = pd.DataFrame() for profile in profiles: - profileDf = pd.DataFrame(profile['bgcMeasKeys']) + profileDf = pd.DataFrame(profile['bgcMeas']) profileDf['cycle_number'] = profile['cycle_number'] profileDf['profile_id'] = profile['_id'] profileDf['lat'] = profile['lat'] @@ -152,7 +165,7 @@ def _parse_into_df(self, profiles): # 24 profiles available reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) - reg_a.search_data(['doxy', 'nitrate'], printURL=True) + reg_a.search_data(['doxy', 'nitrate', 'down_irradiance412'], printURL=True) # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) # reg_a._validate_parameters(['doxy', From eefcbf80299b6d5998b1e0e90c4c1964471bebd5 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 21 Nov 2022 13:09:47 -0500 Subject: [PATCH 032/124] modified to populate prof df by querying individual profiles --- icepyx/quest/dataset_scripts/BGCargo.py | 97 +++++++++++++++++-------- 1 file changed, 66 insertions(+), 31 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 058d2ee60..2a00e516c 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -20,15 +20,15 @@ def _search_data_BGC_helper(self): ''' pass - def search_data(self, params, presRange=None, printURL=False): + def search_data(self, params, presRange=None, printURL=False, keep_all=True): # todo: this currently assumes user specifies exactly two BGC search # params. Need to iterate should the user provide more than 2, and # accommodate if user supplies only 1 param assert len(params) != 0, 'One or more BGC measurements must be specified.' - if not 'pres' in params: - params.append('pres') + # if not 'pres' in params: + # params.append('pres') # validate list of user-entered params, sorts into order to be queried params = self._validate_parameters(params) @@ -69,19 +69,43 @@ def search_data(self, params, presRange=None, printURL=False): print(msg) return - # todo: if additional BGC params (>2 specified), filter results + prof_ids = self._filter_profiles(selectionProfiles, params) - self._filter_profiles(selectionProfiles, params) + print('{0} valid profiles have been identified'.format(len(prof_ids))) + # iterate and download profiles individually + for i in prof_ids: + print("processing profile", i) + self.download_by_profile(i) - # if profiles are found, save them to self as dataframe - self._parse_into_df(selectionProfiles) + self.profiles.reset_index(inplace=True) - # todo: download additional params + if not keep_all: + # todo: drop BGC measurement columns not specified by user + pass + + def _valid_BGC_params(self): ''' - make api request by profile to download additional params - then append the necessary cols to the df + This is a list of valid BGC params, stored here to remove redundancy + They are ordered by how commonly they are measured (approx) ''' + params = valid_params = { + 'pres':0, + 'temp':1, + 'psal':2, + 'cndx':3, + 'doxy':4, + 'ph_in_situ_total':5, + 'chla':6, + 'cdom':7, + 'nitrate':8, + 'bbp700':9, + 'down_irradiance412':10, + 'down_irradiance442':11, + 'down_irradiance490':12, + 'downwelling_par':13, + } + return params def _validate_parameters(self, params): ''' @@ -95,21 +119,7 @@ def _validate_parameters(self, params): ''' # valid params ordered by how commonly they are measured (approx) - valid_params = { - 'pres':0, - 'temp':1, - 'psal':2, - 'cndx':3, - 'doxy':4, - 'chla':5, - 'cdom':6, - 'nitrate':7, - 'bbp700':8, - 'down_irradiance412':9, - 'down_irradiance442':10, - 'down_irradiance490':11, - 'downwelling_par':12, - } + valid_params = self._valid_BGC_params() # checks that params are valid for i in params: @@ -124,6 +134,7 @@ def _filter_profiles(self, profiles, params): ''' from a dictionary of all profiles returned by first API request, remove the profiles that do not contain ALL BGC measurements specified by user + returns a list of profile ID's that contain all necessary BGC params ''' # todo: filter out BGC profiles good_profs = [] @@ -131,12 +142,21 @@ def _filter_profiles(self, profiles, params): bgc_meas = i['bgcMeasKeys'] check = all(item in bgc_meas for item in params) if check: - good_profs.append(i) - print(i['_id']) + good_profs.append(i['_id']) + # print(i['_id']) - profiles = good_profs - print() + # profiles = good_profs + return good_profs + def download_by_profile(self, profile_number): + url = 'https://argovis.colorado.edu/catalog/profiles/{}'.format(profile_number) + resp = requests.get(url) + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + return "Error: Unexpected response {}".format(resp) + profile = resp.json() + self._parse_into_df(profile) + return profile def _parse_into_df(self, profiles): """ @@ -148,7 +168,16 @@ def _parse_into_df(self, profiles): # initialize dict # meas_keys = profiles[0]['bgcMeasKeys'] # df = pd.DataFrame(columns=meas_keys) - df = pd.DataFrame() + + if not isinstance(profiles, list): + profiles = [profiles] + + # initialise the df (empty or containing previously processed profiles) + if not self.profiles is None: + df = self.profiles + else: + df = pd.DataFrame() + for profile in profiles: profileDf = pd.DataFrame(profile['bgcMeas']) profileDf['cycle_number'] = profile['cycle_number'] @@ -157,6 +186,10 @@ def _parse_into_df(self, profiles): profileDf['lon'] = profile['lon'] profileDf['date'] = profile['date'] df = pd.concat([df, profileDf], sort=False) + # if self.profiles is None: + # df = pd.concat([df, profileDf], sort=False) + # else: + # df = df.merge(profileDf, on='profile_id') self.profiles = df if __name__ == '__main__': @@ -165,9 +198,11 @@ def _parse_into_df(self, profiles): # 24 profiles available reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) - reg_a.search_data(['doxy', 'nitrate', 'down_irradiance412'], printURL=True) + reg_a.search_data(['doxy', 'nitrate'], printURL=True) # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) + # reg_a.download_by_profile('4903026_101') + # reg_a._validate_parameters(['doxy', # 'chla', # 'cdomm',]) From 55204d88ca4f3d39386f5e1f0ea81e484c132861 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 28 Nov 2022 12:12:04 -0500 Subject: [PATCH 033/124] finished up BGC argo download! --- icepyx/quest/dataset_scripts/BGCargo.py | 26 ++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 2a00e516c..8dc3f4eb3 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -21,14 +21,12 @@ def _search_data_BGC_helper(self): pass def search_data(self, params, presRange=None, printURL=False, keep_all=True): - # todo: this currently assumes user specifies exactly two BGC search - # params. Need to iterate should the user provide more than 2, and - # accommodate if user supplies only 1 param assert len(params) != 0, 'One or more BGC measurements must be specified.' - # if not 'pres' in params: - # params.append('pres') + # API request requires exactly 2 measurement params, duplicate single of necessary + if len(params) == 1: + params.append(params[0]) # validate list of user-entered params, sorts into order to be queried params = self._validate_parameters(params) @@ -37,8 +35,6 @@ def search_data(self, params, presRange=None, printURL=False, keep_all=True): # builds URL to be submitted baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection/' - # todo: identify which 2 params we specify (ignore physical params) - # todo: special case if only 1 BGC measurment is specified payload = {'startDate': self._start.strftime('%Y-%m-%d'), 'endDate': self._end.strftime('%Y-%m-%d'), 'shape': [self._fmt_coordinates()], @@ -70,6 +66,7 @@ def search_data(self, params, presRange=None, printURL=False, keep_all=True): return + # deterine which profiles contain all specified params prof_ids = self._filter_profiles(selectionProfiles, params) print('{0} valid profiles have been identified'.format(len(prof_ids))) @@ -81,8 +78,13 @@ def search_data(self, params, presRange=None, printURL=False, keep_all=True): self.profiles.reset_index(inplace=True) if not keep_all: - # todo: drop BGC measurement columns not specified by user - pass + # drop BGC measurement columns not specified by user + drop_params = list(set(list(self._valid_BGC_params())[3:]) - set(params)) + qc_params = [] + for i in drop_params: + qc_params.append(i + '_qc') + drop_params += qc_params + self.profiles.drop(columns=drop_params, inplace=True, errors='ignore') def _valid_BGC_params(self): ''' @@ -103,7 +105,8 @@ def _valid_BGC_params(self): 'down_irradiance412':10, 'down_irradiance442':11, 'down_irradiance490':12, - 'downwelling_par':13, + 'down_irradiance380': 13, + 'downwelling_par':14, } return params @@ -198,7 +201,8 @@ def _parse_into_df(self, profiles): # 24 profiles available reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) - reg_a.search_data(['doxy', 'nitrate'], printURL=True) + # reg_a.search_data(['doxy', 'nitrate', 'down_irradiance412'], printURL=True, keep_all=False) + reg_a.search_data(['down_irradiance412'], printURL=True, keep_all=False) # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) # reg_a.download_by_profile('4903026_101') From 0af53d694ef5731b69fe5293ea1be5918f942776 Mon Sep 17 00:00:00 2001 From: Romina Date: Tue, 17 Jan 2023 16:55:58 -0500 Subject: [PATCH 034/124] assert bounding box type in Argo init, begin framework for unit tests --- icepyx/quest/dataset_scripts/argo.py | 1 + icepyx/tests/test_quest_BGCargo.py | 16 ++++++++++++++++ icepyx/tests/test_quest_argo.py | 14 ++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 icepyx/tests/test_quest_BGCargo.py create mode 100644 icepyx/tests/test_quest_argo.py diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 72607d395..3c04c8f56 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -38,6 +38,7 @@ class Argo(DataSet): """ def __init__(self, boundingbox, timeframe): super().__init__(boundingbox, timeframe) + assert self.spatial._ext_type == 'bounding_box' self.profiles = None diff --git a/icepyx/tests/test_quest_BGCargo.py b/icepyx/tests/test_quest_BGCargo.py new file mode 100644 index 000000000..ceadb855f --- /dev/null +++ b/icepyx/tests/test_quest_BGCargo.py @@ -0,0 +1,16 @@ +import icepyx as ipx +import pytest +import warnings + + +def test_available_profiles(): + pass + +def test_no_available_profiles(): + pass + +def test_valid_BGCparams(): + pass + +def test_invalid_BGCparams(): + pass \ No newline at end of file diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py new file mode 100644 index 000000000..a945d7d75 --- /dev/null +++ b/icepyx/tests/test_quest_argo.py @@ -0,0 +1,14 @@ +import icepyx as ipx +import pytest +import warnings + + +def test_available_profiles(): + pass + +def test_no_available_profiles(): + pass + +def test_valid_spatialextent(): + pass + From 27ab9d7c1e2e6a5577c4d60b803b94db9853a89b Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 6 Feb 2023 12:01:28 -0500 Subject: [PATCH 035/124] need to confirm spatial extent is bbox --- icepyx/quest/dataset_scripts/argo.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 3c04c8f56..14567a8de 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -38,7 +38,8 @@ class Argo(DataSet): """ def __init__(self, boundingbox, timeframe): super().__init__(boundingbox, timeframe) - assert self.spatial._ext_type == 'bounding_box' + # todo: need to uncomment this after we rebase + # assert self.spatial._ext_type == 'bounding_box' self.profiles = None @@ -125,4 +126,4 @@ def _parse_into_df(self, profiles): # profiles available reg_a = Argo([-154, 30,-143, 37], ['2022-04-12', '2022-04-26']) reg_a.search_data() - print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) + print(reg_a.profiles[['pres', 'temp', 'psal', 'lat', 'lon']].head()) From 83e0e94ecb705ab3c4b4e591a15541db3c5b0911 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 6 Feb 2023 12:01:51 -0500 Subject: [PATCH 036/124] begin test case for available profiles --- icepyx/tests/test_quest_argo.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index a945d7d75..793b59fc5 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -1,10 +1,15 @@ import icepyx as ipx import pytest import warnings +from icepyx.quest.dataset_scripts.argo import Argo def test_available_profiles(): - pass + reg_a = Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) + reg_a.search_data() + + assert 'temp' in reg_a.profiles.columns + print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) def test_no_available_profiles(): pass From d96c485d3df16a6a43b7597c0576c135f5ffbcaa Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 6 Feb 2023 16:07:09 -0500 Subject: [PATCH 037/124] add tests for argo.py --- icepyx/quest/dataset_scripts/argo.py | 238 ++++++++++++++------------- icepyx/tests/test_quest_argo.py | 53 +++++- 2 files changed, 169 insertions(+), 122 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 3c04c8f56..968ea8f2d 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -1,128 +1,136 @@ from icepyx.quest.dataset_scripts.dataset import DataSet -from icepyx.core.geospatial import geodataframe +from icepyx.core.spatial import geodataframe import requests import pandas as pd import os import numpy as np + class Argo(DataSet): - """ - Initialises an Argo Dataset object - Used to query physical Argo profiles - -> biogeochemical Argo (BGC) not included + """ + Initialises an Argo Dataset object + Used to query physical Argo profiles + -> biogeochemical Argo (BGC) not included - Examples + Examples -------- # example with profiles available >>> reg_a = Argo([-154, 30,-143, 37], ['2022-04-12', '2022-04-26']) - >>> reg_a.search_data() - >>> print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) - pres temp lat lon - 0 3.9 18.608 33.401 -153.913 - 1 5.7 18.598 33.401 -153.913 - 2 7.7 18.588 33.401 -153.913 - 3 9.7 18.462 33.401 -153.913 - 4 11.7 18.378 33.401 -153.913 - - # example with no profiles - >>> reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) - >>> reg_a.search_data() - Warning: Query returned no profiles - Please try different search parameters - - - See Also - -------- - DataSet - GenQuery - """ - def __init__(self, boundingbox, timeframe): - super().__init__(boundingbox, timeframe) - assert self.spatial._ext_type == 'bounding_box' - self.profiles = None - - - def search_data(self, presRange=None, printURL=False): - """ - query dataset given the spatio temporal criteria - and other params specific to the dataset - """ - - # builds URL to be submitted - baseURL = 'https://argovis.colorado.edu/selection/profiles/' - payload = {'startDate': self._start.strftime('%Y-%m-%d'), - 'endDate': self._end.strftime('%Y-%m-%d'), - 'shape': [self._fmt_coordinates()]} - if presRange: - payload['presRange'] = presRange - - # submit request - resp = requests.get(baseURL, params=payload) - - if printURL: - print(resp.url) - - # Consider any status other than 2xx an error - if not resp.status_code // 100 == 2: - msg = "Error: Unexpected response {}".format(resp) - print(msg) - return - - selectionProfiles = resp.json() - - # check for the existence of profiles from query - if selectionProfiles == []: - msg = 'Warning: Query returned no profiles\n' \ - 'Please try different search parameters' - print(msg) - return - - # if profiles are found, save them to self as dataframe - self._parse_into_df(selectionProfiles) - - def _fmt_coordinates(self): - """ - Convert spatial extent into format needed by argovis - i.e. list of polygon coords [[[lat1,lon1],[lat2,lon2],...]] - """ - - gdf = geodataframe(self.extent_type, self._spat_extent) - coordinates_array = np.asarray(gdf.geometry[0].exterior.coords) - x = '' - for i in coordinates_array: - coord = '[{0},{1}]'.format(i[0], i[1]) - if x == '': - x = coord - else: - x += ','+coord - - x = '[['+ x + ']]' - return x - - - def _parse_into_df(self, profiles): - """ - Stores profiles returned by query into dataframe - saves profiles back to self.profiles - returns None - """ - # initialize dict - df = pd.DataFrame() - for profile in profiles: - profileDf = pd.DataFrame(profile['measurements']) - profileDf['cycle_number'] = profile['cycle_number'] - profileDf['profile_id'] = profile['_id'] - profileDf['lat'] = profile['lat'] - profileDf['lon'] = profile['lon'] - profileDf['date'] = profile['date'] - df = pd.concat([df, profileDf], sort=False) - self.profiles = df + >>> reg_a.search_data() + >>> print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) + pres temp lat lon + 0 3.9 18.608 33.401 -153.913 + 1 5.7 18.598 33.401 -153.913 + 2 7.7 18.588 33.401 -153.913 + 3 9.7 18.462 33.401 -153.913 + 4 11.7 18.378 33.401 -153.913 + + # example with no profiles + >>> reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) + >>> reg_a.search_data() + Warning: Query returned no profiles + Please try different search parameters + + + See Also + -------- + DataSet + GenQuery + """ + + def __init__(self, boundingbox, timeframe): + super().__init__(boundingbox, timeframe) + assert self._spatial._ext_type == "bounding_box" + self.profiles = None + + def search_data(self, presRange=None, printURL=False) -> str: + """ + query dataset given the spatio temporal criteria + and other params specific to the dataset + """ + + # builds URL to be submitted + baseURL = "https://argovis.colorado.edu/selection/profiles/" + payload = { + "startDate": self._start.strftime("%Y-%m-%d"), + "endDate": self._end.strftime("%Y-%m-%d"), + "shape": [self._fmt_coordinates()], + } + if presRange: + payload["presRange"] = presRange + + # submit request + resp = requests.get(baseURL, params=payload) + + if printURL: + print(resp.url) + + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + msg = "Error: Unexpected response {}".format(resp) + print(msg) + return msg + + selectionProfiles = resp.json() + + # check for the existence of profiles from query + if selectionProfiles == []: + msg = ( + "Warning: Query returned no profiles\n" + "Please try different search parameters" + ) + print(msg) + return msg + + # if profiles are found, save them to self as dataframe + msg = "Found profiles - converting to a dataframe" + self._parse_into_df(selectionProfiles) + return msg + + def _fmt_coordinates(self) -> str: + """ + Convert spatial extent into string format needed by argovis + i.e. list of polygon coords [[[lat1,lon1],[lat2,lon2],...]] + """ + + gdf = geodataframe(self._spatial._ext_type, self._spatial._spatial_ext) + coordinates_array = np.asarray(gdf.geometry[0].exterior.coords) + x = "" + for i in coordinates_array: + coord = "[{0},{1}]".format(i[0], i[1]) + if x == "": + x = coord + else: + x += "," + coord + + x = "[[" + x + "]]" + return x + + # todo: add a try/except to make sure the json files are valid i.e. contains all data we're expecting (no params are missing) + def _parse_into_df(self, profiles): + """ + Stores profiles returned by query into dataframe + saves profiles back to self.profiles + returns None + """ + # initialize dict + df = pd.DataFrame() + for profile in profiles: + profileDf = pd.DataFrame(profile["measurements"]) + profileDf["cycle_number"] = profile["cycle_number"] + profileDf["profile_id"] = profile["_id"] + profileDf["lat"] = profile["lat"] + profileDf["lon"] = profile["lon"] + profileDf["date"] = profile["date"] + df = pd.concat([df, profileDf], sort=False) + self.profiles = df + # this is just for the purpose of debugging and should be removed later -if __name__ == '__main__': - # no search results - # reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) - # profiles available - reg_a = Argo([-154, 30,-143, 37], ['2022-04-12', '2022-04-26']) - reg_a.search_data() - print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) +if __name__ == "__main__": + # no search results + # reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) + # profiles available + reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + reg_a.search_data() + print(reg_a.profiles[["pres", "temp", "lat", "lon"]].head()) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index 793b59fc5..e0c2663f3 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -5,15 +5,54 @@ def test_available_profiles(): - reg_a = Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) - reg_a.search_data() + reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + obs_msg = reg_a.search_data() + obs_cols = reg_a.profiles.columns + + exp_msg = "Found profiles - converting to a dataframe" + exp_cols = [ + "pres", + "temp", + "cycle_number", + "profile_id", + "lat", + "lon", + "date", + "psal", + ] + + assert obs_msg == exp_msg + assert set(exp_cols) == set(obs_cols) + + # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) - assert 'temp' in reg_a.profiles.columns - print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) def test_no_available_profiles(): - pass + reg_a = Argo([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) + obs = reg_a.search_data() + + exp = ( + "Warning: Query returned no profiles\n" "Please try different search parameters" + ) + + assert obs == exp + + +def test_fmt_coordinates(): + reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + obs = reg_a._fmt_coordinates() + + exp = "[[[-143.0,30.0],[-143.0,37.0],[-154.0,37.0],[-154.0,30.0],[-143.0,30.0]]]" + + assert obs == exp + + +def test_parse_into_df(): + reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + reg_a.search_data() -def test_valid_spatialextent(): - pass + pass + # goal: check number of rows in df matches rows in json + # approach: create json files with profiles and store them in test suite + # then use those for the comparison From 4ec53cd9e870ba8ba7afddefb0b9b7f6fc5d6af2 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 13 Feb 2023 12:45:04 -0500 Subject: [PATCH 038/124] add typing, add example json, and use it to test parsing --- icepyx/quest/dataset_scripts/argo.py | 2 +- icepyx/tests/argovis_test_data.json | 1 + icepyx/tests/test_quest_argo.py | 15 +++++++++++++-- 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 icepyx/tests/argovis_test_data.json diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 968ea8f2d..9fb18760e 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -107,7 +107,7 @@ def _fmt_coordinates(self) -> str: return x # todo: add a try/except to make sure the json files are valid i.e. contains all data we're expecting (no params are missing) - def _parse_into_df(self, profiles): + def _parse_into_df(self, profiles) -> None: """ Stores profiles returned by query into dataframe saves profiles back to self.profiles diff --git a/icepyx/tests/argovis_test_data.json b/icepyx/tests/argovis_test_data.json new file mode 100644 index 000000000..f255b10aa --- /dev/null +++ b/icepyx/tests/argovis_test_data.json @@ -0,0 +1 @@ +[{"_id":"4902461_166","POSITIONING_SYSTEM":"GPS","DATA_CENTRE":"ME","PI_NAME":"Blair Greenan","WMO_INST_TYPE":"865","VERTICAL_SAMPLING_SCHEME":"Primary sampling: averaged","DATA_MODE":"A","PLATFORM_TYPE":"NOVA","measurements":[{"pres":1.7,"psal":32.485,"temp":4.558},{"pres":2.5,"psal":32.485,"temp":4.558},{"pres":3.6,"psal":32.485,"temp":4.558},{"pres":4.6,"psal":32.485,"temp":4.558},{"pres":5.6,"psal":32.485,"temp":4.559},{"pres":6.6,"psal":32.485,"temp":4.56},{"pres":7.6,"psal":32.485,"temp":4.56},{"pres":8.6,"psal":32.485,"temp":4.56},{"pres":9.7,"psal":32.485,"temp":4.561},{"pres":10.7,"psal":32.485,"temp":4.56},{"pres":11.6,"psal":32.485,"temp":4.561},{"pres":12.5,"psal":32.485,"temp":4.56},{"pres":13.6,"psal":32.485,"temp":4.561},{"pres":14.6,"psal":32.485,"temp":4.561},{"pres":15.6,"psal":32.485,"temp":4.56},{"pres":16.6,"psal":32.485,"temp":4.561},{"pres":17.5,"psal":32.486,"temp":4.561},{"pres":18.6,"psal":32.485,"temp":4.561},{"pres":19.6,"psal":32.486,"temp":4.56},{"pres":20.6,"psal":32.485,"temp":4.561},{"pres":21.6,"psal":32.485,"temp":4.56},{"pres":22.7,"psal":32.484,"temp":4.561},{"pres":23.6,"psal":32.485,"temp":4.561},{"pres":24.6,"psal":32.485,"temp":4.561},{"pres":25.6,"psal":32.485,"temp":4.561},{"pres":26.6,"psal":32.485,"temp":4.562},{"pres":27.5,"psal":32.486,"temp":4.562},{"pres":28.6,"psal":32.486,"temp":4.562},{"pres":29.6,"psal":32.486,"temp":4.563},{"pres":30.6,"psal":32.486,"temp":4.563},{"pres":31.6,"psal":32.486,"temp":4.563},{"pres":32.7,"psal":32.497,"temp":4.561},{"pres":33.6,"psal":32.502,"temp":4.561},{"pres":34.6,"psal":32.503,"temp":4.56},{"pres":35.6,"psal":32.506,"temp":4.561},{"pres":36.5,"psal":32.517,"temp":4.561},{"pres":37.6,"psal":32.52,"temp":4.561},{"pres":38.6,"psal":32.524,"temp":4.562},{"pres":39.6,"psal":32.534,"temp":4.563},{"pres":40.6,"psal":32.548,"temp":4.547},{"pres":41.6,"psal":32.548,"temp":4.53},{"pres":42.6,"psal":32.553,"temp":4.517},{"pres":43.6,"psal":32.554,"temp":4.498},{"pres":44.6,"psal":32.555,"temp":4.488},{"pres":45.6,"psal":32.557,"temp":4.482},{"pres":46.6,"psal":32.557,"temp":4.481},{"pres":47.6,"psal":32.558,"temp":4.48},{"pres":48.6,"psal":32.559,"temp":4.481},{"pres":49.6,"psal":32.561,"temp":4.479},{"pres":50.6,"psal":32.567,"temp":4.479},{"pres":51.6,"psal":32.571,"temp":4.477},{"pres":52.6,"psal":32.575,"temp":4.476},{"pres":53.6,"psal":32.583,"temp":4.488},{"pres":54.6,"psal":32.606,"temp":4.541},{"pres":55.6,"psal":32.627,"temp":4.546},{"pres":56.6,"psal":32.628,"temp":4.55},{"pres":57.6,"psal":32.634,"temp":4.557},{"pres":58.6,"psal":32.64,"temp":4.562},{"pres":59.6,"psal":32.641,"temp":4.569},{"pres":60.6,"psal":32.664,"temp":4.605},{"pres":61.6,"psal":32.684,"temp":4.636},{"pres":62.6,"psal":32.729,"temp":4.685},{"pres":63.5,"psal":32.772,"temp":4.719},{"pres":64.6,"psal":32.793,"temp":4.75},{"pres":65.6,"psal":32.833,"temp":4.788},{"pres":66.6,"psal":32.872,"temp":4.842},{"pres":67.6,"psal":32.921,"temp":4.894},{"pres":68.6,"psal":32.958,"temp":4.985},{"pres":69.6,"psal":33.052,"temp":5.058},{"pres":70.6,"psal":33.097,"temp":5.081},{"pres":71.6,"psal":33.137,"temp":5.121},{"pres":72.6,"psal":33.224,"temp":5.16},{"pres":73.6,"psal":33.267,"temp":5.187},{"pres":74.6,"psal":33.309,"temp":5.226},{"pres":75.6,"psal":33.379,"temp":5.258},{"pres":76.6,"psal":33.388,"temp":5.262},{"pres":77.6,"psal":33.408,"temp":5.287},{"pres":78.6,"psal":33.432,"temp":5.298},{"pres":79.6,"psal":33.436,"temp":5.299},{"pres":80.6,"psal":33.441,"temp":5.304},{"pres":81.7,"psal":33.46,"temp":5.308},{"pres":82.7,"psal":33.46,"temp":5.304},{"pres":83.7,"psal":33.464,"temp":5.308},{"pres":84.7,"psal":33.472,"temp":5.31},{"pres":85.7,"psal":33.48,"temp":5.285},{"pres":86.6,"psal":33.492,"temp":5.278},{"pres":87.6,"psal":33.519,"temp":5.287},{"pres":88.7,"psal":33.524,"temp":5.282},{"pres":89.7,"psal":33.531,"temp":5.289},{"pres":90.7,"psal":33.535,"temp":5.295},{"pres":91.6,"psal":33.542,"temp":5.297},{"pres":92.6,"psal":33.556,"temp":5.3},{"pres":93.6,"psal":33.583,"temp":5.299},{"pres":94.6,"psal":33.601,"temp":5.302},{"pres":95.6,"psal":33.612,"temp":5.301},{"pres":96.6,"psal":33.615,"temp":5.298},{"pres":97.6,"psal":33.615,"temp":5.299},{"pres":98.6,"psal":33.623,"temp":5.298},{"pres":99.7,"psal":33.634,"temp":5.295},{"pres":102.2,"psal":33.664,"temp":5.298},{"pres":104.7,"psal":33.681,"temp":5.297},{"pres":107.1,"psal":33.7,"temp":5.342},{"pres":109.6,"psal":33.734,"temp":5.351},{"pres":112.1,"psal":33.746,"temp":5.373},{"pres":114.7,"psal":33.762,"temp":5.424},{"pres":117.1,"psal":33.779,"temp":5.441},{"pres":119.6,"psal":33.783,"temp":5.437},{"pres":122.1,"psal":33.793,"temp":5.374},{"pres":124.6,"psal":33.791,"temp":5.329},{"pres":127.1,"psal":33.792,"temp":5.309},{"pres":129.6,"psal":33.791,"temp":5.297},{"pres":132.1,"psal":33.79,"temp":5.287},{"pres":134.5,"psal":33.792,"temp":5.242},{"pres":137,"psal":33.789,"temp":5.131},{"pres":139.6,"psal":33.78,"temp":5.081},{"pres":142.2,"psal":33.783,"temp":4.975},{"pres":144.6,"psal":33.777,"temp":4.899},{"pres":147.1,"psal":33.782,"temp":4.844},{"pres":149.6,"psal":33.78,"temp":4.822},{"pres":152.1,"psal":33.784,"temp":4.808},{"pres":154.6,"psal":33.786,"temp":4.775},{"pres":157.1,"psal":33.787,"temp":4.763},{"pres":159.6,"psal":33.794,"temp":4.75},{"pres":162,"psal":33.798,"temp":4.719},{"pres":164.6,"psal":33.797,"temp":4.697},{"pres":167.1,"psal":33.8,"temp":4.69},{"pres":169.6,"psal":33.802,"temp":4.685},{"pres":172.1,"psal":33.805,"temp":4.666},{"pres":174.6,"psal":33.804,"temp":4.634},{"pres":177.1,"psal":33.808,"temp":4.644},{"pres":179.5,"psal":33.814,"temp":4.652},{"pres":182.1,"psal":33.819,"temp":4.588},{"pres":184.6,"psal":33.816,"temp":4.535},{"pres":187.1,"psal":33.823,"temp":4.533},{"pres":189.6,"psal":33.826,"temp":4.506},{"pres":192.1,"psal":33.831,"temp":4.538},{"pres":194.6,"psal":33.839,"temp":4.555},{"pres":197.1,"psal":33.849,"temp":4.569},{"pres":199.6,"psal":33.851,"temp":4.573},{"pres":202.1,"psal":33.861,"temp":4.592},{"pres":204.6,"psal":33.865,"temp":4.603},{"pres":207.1,"psal":33.868,"temp":4.6},{"pres":209.6,"psal":33.869,"temp":4.597},{"pres":212.1,"psal":33.87,"temp":4.595},{"pres":214.6,"psal":33.872,"temp":4.592},{"pres":217.1,"psal":33.876,"temp":4.574},{"pres":219.6,"psal":33.877,"temp":4.563},{"pres":222.1,"psal":33.878,"temp":4.56},{"pres":224.7,"psal":33.878,"temp":4.559},{"pres":227.1,"psal":33.879,"temp":4.551},{"pres":229.6,"psal":33.886,"temp":4.518},{"pres":232.1,"psal":33.892,"temp":4.519},{"pres":234.7,"psal":33.899,"temp":4.517},{"pres":237.1,"psal":33.901,"temp":4.515},{"pres":239.6,"psal":33.904,"temp":4.523},{"pres":242.1,"psal":33.91,"temp":4.539},{"pres":244.6,"psal":33.916,"temp":4.544},{"pres":247.1,"psal":33.921,"temp":4.534},{"pres":249.6,"psal":33.924,"temp":4.503},{"pres":252.2,"psal":33.923,"temp":4.488},{"pres":254.6,"psal":33.925,"temp":4.47},{"pres":257.1,"psal":33.929,"temp":4.475},{"pres":259.6,"psal":33.934,"temp":4.474},{"pres":262,"psal":33.936,"temp":4.472},{"pres":264.6,"psal":33.937,"temp":4.467},{"pres":267.1,"psal":33.938,"temp":4.462},{"pres":269.6,"psal":33.938,"temp":4.458},{"pres":272.1,"psal":33.939,"temp":4.455},{"pres":274.7,"psal":33.942,"temp":4.448},{"pres":277.1,"psal":33.946,"temp":4.431},{"pres":279.5,"psal":33.945,"temp":4.42},{"pres":282.1,"psal":33.946,"temp":4.407},{"pres":284.6,"psal":33.946,"temp":4.401},{"pres":287.1,"psal":33.946,"temp":4.398},{"pres":289.6,"psal":33.947,"temp":4.391},{"pres":292.1,"psal":33.953,"temp":4.4},{"pres":294.6,"psal":33.956,"temp":4.398},{"pres":297.1,"psal":33.963,"temp":4.433},{"pres":299.6,"psal":33.969,"temp":4.436},{"pres":302.1,"psal":33.972,"temp":4.427},{"pres":304.6,"psal":33.975,"temp":4.418},{"pres":307.1,"psal":33.978,"temp":4.41},{"pres":309.6,"psal":33.98,"temp":4.405},{"pres":312.1,"psal":33.981,"temp":4.403},{"pres":314.5,"psal":33.982,"temp":4.401},{"pres":317.1,"psal":33.985,"temp":4.394},{"pres":319.6,"psal":33.987,"temp":4.388},{"pres":322.1,"psal":33.99,"temp":4.381},{"pres":324.6,"psal":33.992,"temp":4.376},{"pres":327.2,"psal":33.997,"temp":4.368},{"pres":329.7,"psal":34,"temp":4.361},{"pres":332.1,"psal":34.002,"temp":4.357},{"pres":334.6,"psal":34.004,"temp":4.354},{"pres":337.1,"psal":34.006,"temp":4.35},{"pres":339.6,"psal":34.007,"temp":4.348},{"pres":342.1,"psal":34.011,"temp":4.341},{"pres":344.7,"psal":34.014,"temp":4.334},{"pres":347.1,"psal":34.016,"temp":4.33},{"pres":349.6,"psal":34.02,"temp":4.322},{"pres":352.1,"psal":34.022,"temp":4.318},{"pres":354.6,"psal":34.025,"temp":4.309},{"pres":357.1,"psal":34.027,"temp":4.304},{"pres":359.6,"psal":34.029,"temp":4.295},{"pres":362.1,"psal":34.031,"temp":4.29},{"pres":364.6,"psal":34.034,"temp":4.282},{"pres":367.1,"psal":34.039,"temp":4.268},{"pres":369.6,"psal":34.041,"temp":4.261},{"pres":372.1,"psal":34.043,"temp":4.258},{"pres":374.6,"psal":34.047,"temp":4.25},{"pres":377.1,"psal":34.05,"temp":4.244},{"pres":379.6,"psal":34.054,"temp":4.237},{"pres":382.1,"psal":34.056,"temp":4.232},{"pres":384.6,"psal":34.058,"temp":4.225},{"pres":387.1,"psal":34.06,"temp":4.219},{"pres":389.6,"psal":34.061,"temp":4.212},{"pres":392.1,"psal":34.064,"temp":4.208},{"pres":394.6,"psal":34.065,"temp":4.205},{"pres":397.1,"psal":34.066,"temp":4.203},{"pres":399.6,"psal":34.068,"temp":4.201},{"pres":402.1,"psal":34.069,"temp":4.199},{"pres":404.6,"psal":34.072,"temp":4.199},{"pres":407.1,"psal":34.078,"temp":4.19},{"pres":409.5,"psal":34.079,"temp":4.182},{"pres":412.1,"psal":34.08,"temp":4.178},{"pres":414.6,"psal":34.082,"temp":4.173},{"pres":417.1,"psal":34.084,"temp":4.169},{"pres":419.7,"psal":34.087,"temp":4.16},{"pres":422.2,"psal":34.09,"temp":4.154},{"pres":424.6,"psal":34.092,"temp":4.148},{"pres":427.1,"psal":34.093,"temp":4.145},{"pres":429.6,"psal":34.096,"temp":4.137},{"pres":432,"psal":34.098,"temp":4.13},{"pres":434.6,"psal":34.098,"temp":4.128},{"pres":437.1,"psal":34.101,"temp":4.12},{"pres":439.6,"psal":34.103,"temp":4.113},{"pres":442.1,"psal":34.105,"temp":4.106},{"pres":444.6,"psal":34.107,"temp":4.1},{"pres":447.1,"psal":34.11,"temp":4.091},{"pres":449.6,"psal":34.113,"temp":4.084},{"pres":452.2,"psal":34.115,"temp":4.077},{"pres":454.6,"psal":34.118,"temp":4.068},{"pres":457.1,"psal":34.118,"temp":4.066},{"pres":459.5,"psal":34.119,"temp":4.06},{"pres":462.1,"psal":34.12,"temp":4.052},{"pres":464.6,"psal":34.119,"temp":4.039},{"pres":467.1,"psal":34.119,"temp":4.036},{"pres":469.6,"psal":34.122,"temp":4.026},{"pres":472.1,"psal":34.125,"temp":4.012},{"pres":474.6,"psal":34.126,"temp":4.003},{"pres":477.1,"psal":34.127,"temp":3.994},{"pres":479.6,"psal":34.128,"temp":3.986},{"pres":482.1,"psal":34.129,"temp":3.976},{"pres":484.6,"psal":34.132,"temp":3.959},{"pres":487.1,"psal":34.133,"temp":3.948},{"pres":489.6,"psal":34.134,"temp":3.942},{"pres":492.1,"psal":34.136,"temp":3.941},{"pres":494.7,"psal":34.141,"temp":3.941},{"pres":497.1,"psal":34.141,"temp":3.934},{"pres":499.6,"psal":34.145,"temp":3.943},{"pres":504.6,"psal":34.15,"temp":3.934},{"pres":509.6,"psal":34.154,"temp":3.918},{"pres":514.6,"psal":34.157,"temp":3.912},{"pres":519.6,"psal":34.159,"temp":3.912},{"pres":524.6,"psal":34.164,"temp":3.906},{"pres":529.6,"psal":34.167,"temp":3.89},{"pres":534.7,"psal":34.173,"temp":3.872},{"pres":539.7,"psal":34.175,"temp":3.864},{"pres":544.7,"psal":34.178,"temp":3.857},{"pres":549.6,"psal":34.182,"temp":3.845},{"pres":554.6,"psal":34.184,"temp":3.838},{"pres":559.6,"psal":34.187,"temp":3.828},{"pres":564.5,"psal":34.191,"temp":3.809},{"pres":569.6,"psal":34.193,"temp":3.797},{"pres":574.6,"psal":34.196,"temp":3.79},{"pres":579.6,"psal":34.201,"temp":3.774},{"pres":584.6,"psal":34.203,"temp":3.765},{"pres":589.6,"psal":34.205,"temp":3.762},{"pres":594.6,"psal":34.21,"temp":3.749},{"pres":599.6,"psal":34.213,"temp":3.731},{"pres":604.6,"psal":34.218,"temp":3.719},{"pres":609.6,"psal":34.223,"temp":3.725},{"pres":614.6,"psal":34.226,"temp":3.708},{"pres":619.6,"psal":34.226,"temp":3.696},{"pres":624.6,"psal":34.227,"temp":3.686},{"pres":629.6,"psal":34.228,"temp":3.677},{"pres":634.7,"psal":34.228,"temp":3.671},{"pres":639.6,"psal":34.229,"temp":3.668},{"pres":644.6,"psal":34.232,"temp":3.653},{"pres":649.6,"psal":34.234,"temp":3.633},{"pres":654.6,"psal":34.232,"temp":3.582},{"pres":659.6,"psal":34.23,"temp":3.552},{"pres":664.6,"psal":34.23,"temp":3.527},{"pres":669.6,"psal":34.234,"temp":3.515},{"pres":674.6,"psal":34.233,"temp":3.499},{"pres":679.6,"psal":34.233,"temp":3.483},{"pres":684.5,"psal":34.236,"temp":3.471},{"pres":689.5,"psal":34.238,"temp":3.472},{"pres":694.6,"psal":34.242,"temp":3.475},{"pres":699.7,"psal":34.247,"temp":3.472},{"pres":704.6,"psal":34.25,"temp":3.46},{"pres":709.6,"psal":34.25,"temp":3.45},{"pres":714.6,"psal":34.253,"temp":3.441},{"pres":719.6,"psal":34.256,"temp":3.429},{"pres":724.7,"psal":34.257,"temp":3.42},{"pres":729.6,"psal":34.259,"temp":3.412},{"pres":734.6,"psal":34.261,"temp":3.407},{"pres":739.6,"psal":34.264,"temp":3.395},{"pres":744.6,"psal":34.265,"temp":3.391},{"pres":749.6,"psal":34.266,"temp":3.386},{"pres":754.6,"psal":34.267,"temp":3.379},{"pres":759.6,"psal":34.268,"temp":3.37},{"pres":764.6,"psal":34.269,"temp":3.357},{"pres":769.6,"psal":34.27,"temp":3.35},{"pres":774.6,"psal":34.272,"temp":3.341},{"pres":779.6,"psal":34.272,"temp":3.333},{"pres":784.6,"psal":34.273,"temp":3.325},{"pres":789.6,"psal":34.277,"temp":3.309},{"pres":794.7,"psal":34.282,"temp":3.294},{"pres":799.6,"psal":34.284,"temp":3.288},{"pres":804.6,"psal":34.287,"temp":3.28},{"pres":809.6,"psal":34.288,"temp":3.277},{"pres":814.7,"psal":34.291,"temp":3.268},{"pres":819.6,"psal":34.294,"temp":3.252},{"pres":824.6,"psal":34.296,"temp":3.236},{"pres":829.6,"psal":34.296,"temp":3.23},{"pres":834.6,"psal":34.298,"temp":3.221},{"pres":839.6,"psal":34.3,"temp":3.213},{"pres":844.6,"psal":34.303,"temp":3.201},{"pres":849.6,"psal":34.306,"temp":3.193},{"pres":854.6,"psal":34.307,"temp":3.181},{"pres":859.6,"psal":34.308,"temp":3.174},{"pres":864.6,"psal":34.31,"temp":3.162},{"pres":869.6,"psal":34.315,"temp":3.145},{"pres":874.6,"psal":34.316,"temp":3.138},{"pres":879.6,"psal":34.317,"temp":3.136},{"pres":884.6,"psal":34.319,"temp":3.13},{"pres":889.6,"psal":34.32,"temp":3.126},{"pres":894.6,"psal":34.321,"temp":3.122},{"pres":899.6,"psal":34.324,"temp":3.112},{"pres":904.6,"psal":34.326,"temp":3.105},{"pres":909.6,"psal":34.328,"temp":3.094},{"pres":914.6,"psal":34.329,"temp":3.09},{"pres":919.6,"psal":34.331,"temp":3.083},{"pres":924.6,"psal":34.332,"temp":3.076},{"pres":929.6,"psal":34.333,"temp":3.074},{"pres":934.6,"psal":34.335,"temp":3.068},{"pres":939.6,"psal":34.336,"temp":3.059},{"pres":944.7,"psal":34.34,"temp":3.047},{"pres":949.6,"psal":34.341,"temp":3.039},{"pres":954.6,"psal":34.343,"temp":3.035},{"pres":959.6,"psal":34.346,"temp":3.025},{"pres":964.5,"psal":34.349,"temp":3.011},{"pres":969.5,"psal":34.351,"temp":2.999},{"pres":974.6,"psal":34.355,"temp":2.984},{"pres":979.6,"psal":34.357,"temp":2.976},{"pres":984.6,"psal":34.358,"temp":2.978},{"pres":989.6,"psal":34.36,"temp":2.971},{"pres":994.6,"psal":34.361,"temp":2.967},{"pres":999.7,"psal":34.364,"temp":2.959},{"pres":1004.6,"psal":34.365,"temp":2.954},{"pres":1009.6,"psal":34.366,"temp":2.948},{"pres":1014.6,"psal":34.369,"temp":2.936},{"pres":1019.7,"psal":34.37,"temp":2.928},{"pres":1024.6,"psal":34.372,"temp":2.924},{"pres":1029.6,"psal":34.374,"temp":2.914},{"pres":1034.5,"psal":34.375,"temp":2.908},{"pres":1039.6,"psal":34.377,"temp":2.897},{"pres":1044.6,"psal":34.379,"temp":2.888},{"pres":1049.6,"psal":34.381,"temp":2.879},{"pres":1054.6,"psal":34.381,"temp":2.874},{"pres":1059.6,"psal":34.383,"temp":2.866},{"pres":1064.6,"psal":34.385,"temp":2.856},{"pres":1069.7,"psal":34.387,"temp":2.844},{"pres":1074.6,"psal":34.389,"temp":2.838},{"pres":1079.6,"psal":34.392,"temp":2.825},{"pres":1084.7,"psal":34.394,"temp":2.815},{"pres":1089.6,"psal":34.396,"temp":2.81},{"pres":1094.6,"psal":34.397,"temp":2.809},{"pres":1099.6,"psal":34.4,"temp":2.798},{"pres":1104.6,"psal":34.4,"temp":2.793},{"pres":1109.6,"psal":34.401,"temp":2.787},{"pres":1114.6,"psal":34.403,"temp":2.778},{"pres":1119.6,"psal":34.405,"temp":2.772},{"pres":1124.6,"psal":34.406,"temp":2.77},{"pres":1129.6,"psal":34.406,"temp":2.766},{"pres":1134.6,"psal":34.408,"temp":2.761},{"pres":1139.7,"psal":34.41,"temp":2.751},{"pres":1144.6,"psal":34.411,"temp":2.746},{"pres":1149.7,"psal":34.413,"temp":2.735},{"pres":1154.6,"psal":34.414,"temp":2.73},{"pres":1159.6,"psal":34.415,"temp":2.725},{"pres":1164.6,"psal":34.415,"temp":2.724},{"pres":1169.6,"psal":34.416,"temp":2.722},{"pres":1174.6,"psal":34.417,"temp":2.718},{"pres":1179.7,"psal":34.417,"temp":2.717},{"pres":1184.6,"psal":34.418,"temp":2.714},{"pres":1189.6,"psal":34.419,"temp":2.708},{"pres":1194.5,"psal":34.42,"temp":2.702},{"pres":1199.5,"psal":34.421,"temp":2.697},{"pres":1204.6,"psal":34.422,"temp":2.693},{"pres":1209.7,"psal":34.424,"temp":2.686},{"pres":1214.6,"psal":34.426,"temp":2.674},{"pres":1219.6,"psal":34.426,"temp":2.672},{"pres":1224.6,"psal":34.428,"temp":2.665},{"pres":1229.6,"psal":34.43,"temp":2.653},{"pres":1234.6,"psal":34.431,"temp":2.649},{"pres":1239.6,"psal":34.433,"temp":2.64},{"pres":1244.6,"psal":34.434,"temp":2.634},{"pres":1249.6,"psal":34.436,"temp":2.628},{"pres":1254.7,"psal":34.436,"temp":2.627},{"pres":1259.6,"psal":34.437,"temp":2.622},{"pres":1264.6,"psal":34.438,"temp":2.616},{"pres":1269.6,"psal":34.439,"temp":2.61},{"pres":1274.6,"psal":34.44,"temp":2.606},{"pres":1279.6,"psal":34.442,"temp":2.601},{"pres":1284.6,"psal":34.443,"temp":2.597},{"pres":1289.6,"psal":34.443,"temp":2.593},{"pres":1294.6,"psal":34.444,"temp":2.588},{"pres":1299.6,"psal":34.446,"temp":2.579},{"pres":1304.6,"psal":34.448,"temp":2.57},{"pres":1309.6,"psal":34.449,"temp":2.564},{"pres":1314.6,"psal":34.45,"temp":2.56},{"pres":1319.6,"psal":34.451,"temp":2.556},{"pres":1324.6,"psal":34.452,"temp":2.551},{"pres":1329.7,"psal":34.454,"temp":2.543},{"pres":1334.6,"psal":34.455,"temp":2.539},{"pres":1339.6,"psal":34.455,"temp":2.535},{"pres":1344.5,"psal":34.456,"temp":2.534},{"pres":1349.5,"psal":34.456,"temp":2.532},{"pres":1354.6,"psal":34.457,"temp":2.526},{"pres":1359.6,"psal":34.458,"temp":2.521},{"pres":1364.6,"psal":34.46,"temp":2.512},{"pres":1369.6,"psal":34.461,"temp":2.508},{"pres":1374.5,"psal":34.462,"temp":2.501},{"pres":1379.5,"psal":34.463,"temp":2.497},{"pres":1384.6,"psal":34.465,"temp":2.489},{"pres":1389.7,"psal":34.465,"temp":2.484},{"pres":1394.6,"psal":34.465,"temp":2.482},{"pres":1399.6,"psal":34.468,"temp":2.47},{"pres":1404.6,"psal":34.47,"temp":2.463},{"pres":1409.6,"psal":34.471,"temp":2.458},{"pres":1414.6,"psal":34.471,"temp":2.456},{"pres":1419.6,"psal":34.472,"temp":2.451},{"pres":1424.7,"psal":34.474,"temp":2.443},{"pres":1429.6,"psal":34.476,"temp":2.432},{"pres":1434.6,"psal":34.477,"temp":2.426},{"pres":1439.7,"psal":34.479,"temp":2.416},{"pres":1444.6,"psal":34.481,"temp":2.41},{"pres":1449.5,"psal":34.481,"temp":2.408},{"pres":1454.5,"psal":34.481,"temp":2.408},{"pres":1459.6,"psal":34.482,"temp":2.405},{"pres":1464.6,"psal":34.484,"temp":2.395},{"pres":1469.6,"psal":34.484,"temp":2.392},{"pres":1474.6,"psal":34.485,"temp":2.387},{"pres":1479.6,"psal":34.487,"temp":2.38},{"pres":1484.6,"psal":34.489,"temp":2.37},{"pres":1489.6,"psal":34.49,"temp":2.365},{"pres":1494.6,"psal":34.491,"temp":2.361},{"pres":1499.6,"psal":34.492,"temp":2.357},{"pres":1504.6,"psal":34.493,"temp":2.35},{"pres":1509.6,"psal":34.495,"temp":2.342},{"pres":1514.5,"psal":34.496,"temp":2.338},{"pres":1519.6,"psal":34.496,"temp":2.335},{"pres":1524.6,"psal":34.498,"temp":2.329},{"pres":1529.6,"psal":34.499,"temp":2.323},{"pres":1534.6,"psal":34.501,"temp":2.315},{"pres":1539.6,"psal":34.502,"temp":2.308},{"pres":1544.6,"psal":34.502,"temp":2.308},{"pres":1549.5,"psal":34.503,"temp":2.306},{"pres":1554.5,"psal":34.504,"temp":2.302},{"pres":1559.5,"psal":34.505,"temp":2.296},{"pres":1564.6,"psal":34.506,"temp":2.289},{"pres":1569.6,"psal":34.507,"temp":2.285},{"pres":1574.6,"psal":34.508,"temp":2.281},{"pres":1579.7,"psal":34.509,"temp":2.278},{"pres":1584.6,"psal":34.509,"temp":2.275},{"pres":1589.6,"psal":34.51,"temp":2.272},{"pres":1594.6,"psal":34.511,"temp":2.269},{"pres":1599.6,"psal":34.511,"temp":2.267},{"pres":1604.6,"psal":34.512,"temp":2.265},{"pres":1609.6,"psal":34.512,"temp":2.261},{"pres":1614.6,"psal":34.513,"temp":2.258},{"pres":1619.6,"psal":34.514,"temp":2.255},{"pres":1624.6,"psal":34.514,"temp":2.252},{"pres":1629.6,"psal":34.516,"temp":2.247},{"pres":1634.6,"psal":34.516,"temp":2.243},{"pres":1639.6,"psal":34.518,"temp":2.236},{"pres":1644.6,"psal":34.519,"temp":2.231},{"pres":1649.6,"psal":34.52,"temp":2.227},{"pres":1654.6,"psal":34.521,"temp":2.219},{"pres":1659.5,"psal":34.522,"temp":2.215},{"pres":1664.6,"psal":34.524,"temp":2.205},{"pres":1669.7,"psal":34.526,"temp":2.196},{"pres":1674.6,"psal":34.527,"temp":2.192},{"pres":1679.6,"psal":34.528,"temp":2.187},{"pres":1684.6,"psal":34.529,"temp":2.182},{"pres":1689.7,"psal":34.53,"temp":2.175},{"pres":1694.6,"psal":34.531,"temp":2.17},{"pres":1699.6,"psal":34.533,"temp":2.162},{"pres":1704.6,"psal":34.534,"temp":2.154},{"pres":1709.5,"psal":34.536,"temp":2.146},{"pres":1714.6,"psal":34.537,"temp":2.14},{"pres":1719.6,"psal":34.539,"temp":2.133},{"pres":1724.7,"psal":34.54,"temp":2.128},{"pres":1729.7,"psal":34.541,"temp":2.119},{"pres":1734.6,"psal":34.542,"temp":2.114},{"pres":1739.6,"psal":34.544,"temp":2.107},{"pres":1744.6,"psal":34.544,"temp":2.104},{"pres":1749.6,"psal":34.546,"temp":2.097},{"pres":1754.6,"psal":34.547,"temp":2.089},{"pres":1759.5,"psal":34.548,"temp":2.083},{"pres":1764.5,"psal":34.551,"temp":2.073},{"pres":1769.5,"psal":34.552,"temp":2.065},{"pres":1774.6,"psal":34.553,"temp":2.063},{"pres":1779.6,"psal":34.553,"temp":2.062},{"pres":1784.6,"psal":34.553,"temp":2.06},{"pres":1789.7,"psal":34.554,"temp":2.055},{"pres":1794.7,"psal":34.555,"temp":2.051},{"pres":1799.6,"psal":34.556,"temp":2.048},{"pres":1804.6,"psal":34.556,"temp":2.045},{"pres":1809.7,"psal":34.557,"temp":2.041},{"pres":1814.6,"psal":34.557,"temp":2.039},{"pres":1819.6,"psal":34.559,"temp":2.034},{"pres":1824.6,"psal":34.56,"temp":2.027},{"pres":1829.6,"psal":34.56,"temp":2.024},{"pres":1834.6,"psal":34.561,"temp":2.02},{"pres":1839.6,"psal":34.562,"temp":2.015},{"pres":1844.5,"psal":34.563,"temp":2.012},{"pres":1849.5,"psal":34.564,"temp":2.008},{"pres":1854.6,"psal":34.564,"temp":2.006},{"pres":1859.6,"psal":34.566,"temp":1.999},{"pres":1864.6,"psal":34.567,"temp":1.994},{"pres":1869.7,"psal":34.568,"temp":1.985},{"pres":1874.6,"psal":34.57,"temp":1.979},{"pres":1879.6,"psal":34.57,"temp":1.974},{"pres":1884.6,"psal":34.571,"temp":1.969},{"pres":1889.6,"psal":34.572,"temp":1.965},{"pres":1894.6,"psal":34.574,"temp":1.957},{"pres":1899.6,"psal":34.575,"temp":1.952},{"pres":1904.5,"psal":34.576,"temp":1.947},{"pres":1909.4,"psal":34.576,"temp":1.944},{"pres":1914.6,"psal":34.577,"temp":1.943},{"pres":1919.6,"psal":34.577,"temp":1.939},{"pres":1924.6,"psal":34.578,"temp":1.937},{"pres":1929.6,"psal":34.579,"temp":1.933},{"pres":1934.7,"psal":34.579,"temp":1.932},{"pres":1939.6,"psal":34.58,"temp":1.928},{"pres":1944.6,"psal":34.58,"temp":1.925},{"pres":1949.5,"psal":34.581,"temp":1.923},{"pres":1954.5,"psal":34.581,"temp":1.922},{"pres":1959.5,"psal":34.582,"temp":1.916},{"pres":1964.6,"psal":34.583,"temp":1.91},{"pres":1969.6,"psal":34.584,"temp":1.909},{"pres":1974.6,"psal":34.584,"temp":1.908},{"pres":1979.6,"psal":34.584,"temp":1.905},{"pres":1984.6,"psal":34.585,"temp":1.9},{"pres":1989.6,"psal":34.586,"temp":1.897},{"pres":1994.6,"psal":34.586,"temp":1.895},{"pres":1999.6,"psal":34.587,"temp":1.894},{"pres":2004.6,"psal":34.587,"temp":1.892},{"pres":2009.6,"psal":34.588,"temp":1.89},{"pres":2014.6,"psal":34.588,"temp":1.887},{"pres":2019.6,"psal":34.588,"temp":1.886}],"station_parameters":["pres","psal","temp"],"pres_max_for_TEMP":2019.6,"pres_min_for_TEMP":1.7,"pres_max_for_PSAL":2019.6,"pres_min_for_PSAL":1.7,"max_pres":2019.6,"date":"2023-01-26T05:55:00.000Z","date_added":"2023-01-27T08:08:38.067Z","date_qc":1,"lat":55.615684509277344,"lon":-153.22265625,"geoLocation":{"type":"Point","coordinates":[-153.22265625,55.615684509277344]},"position_qc":1,"cycle_number":166,"dac":"meds","platform_number":4902461,"station_parameters_in_nc":["MTIME","PRES","PSAL","TEMP"],"nc_url":"ftp://ftp.ifremer.fr/ifremer/argo/dac/meds/4902461/profiles/R4902461_166.nc","DIRECTION":"A","BASIN":2,"core_data_mode":"A","roundLat":"55.616","roundLon":"-153.223","strLat":"55.616 N","strLon":"153.223 W","formatted_station_parameters":[" pres"," psal"," temp"]},{"_id":"4902521_56","POSITIONING_SYSTEM":"GPS","DATA_CENTRE":"ME","PI_NAME":"Blair Greenan","WMO_INST_TYPE":"844","VERTICAL_SAMPLING_SCHEME":"Primary sampling: averaged","DATA_MODE":"R","PLATFORM_TYPE":"ARVOR","measurements":[{"pres":3.4,"psal":32.497,"temp":4.854},{"pres":4,"psal":32.498,"temp":4.852},{"pres":5.1,"psal":32.498,"temp":4.852},{"pres":6,"psal":32.498,"temp":4.853},{"pres":7.1,"psal":32.498,"temp":4.854},{"pres":7.9,"psal":32.498,"temp":4.855},{"pres":9,"psal":32.498,"temp":4.855},{"pres":10.1,"psal":32.497,"temp":4.856},{"pres":11.1,"psal":32.498,"temp":4.857},{"pres":12.1,"psal":32.498,"temp":4.855},{"pres":13.1,"psal":32.498,"temp":4.854},{"pres":14,"psal":32.498,"temp":4.855},{"pres":15.2,"psal":32.498,"temp":4.855},{"pres":16.1,"psal":32.498,"temp":4.855},{"pres":16.8,"psal":32.498,"temp":4.854},{"pres":17.8,"psal":32.498,"temp":4.858},{"pres":18.8,"psal":32.499,"temp":4.858},{"pres":19.9,"psal":32.498,"temp":4.858},{"pres":21,"psal":32.498,"temp":4.859},{"pres":22.1,"psal":32.498,"temp":4.859},{"pres":22.9,"psal":32.499,"temp":4.859},{"pres":23.7,"psal":32.498,"temp":4.859},{"pres":24.8,"psal":32.498,"temp":4.859},{"pres":25.9,"psal":32.498,"temp":4.859},{"pres":26.9,"psal":32.498,"temp":4.859},{"pres":27.9,"psal":32.499,"temp":4.859},{"pres":29.1,"psal":32.5,"temp":4.858},{"pres":30.1,"psal":32.499,"temp":4.858},{"pres":31.1,"psal":32.499,"temp":4.858},{"pres":32.1,"psal":32.499,"temp":4.859},{"pres":33.1,"psal":32.499,"temp":4.858},{"pres":34.1,"psal":32.5,"temp":4.857},{"pres":35.1,"psal":32.5,"temp":4.856},{"pres":35.9,"psal":32.501,"temp":4.856},{"pres":37,"psal":32.502,"temp":4.854},{"pres":38,"psal":32.502,"temp":4.855},{"pres":39,"psal":32.502,"temp":4.854},{"pres":40,"psal":32.502,"temp":4.854},{"pres":41.1,"psal":32.502,"temp":4.853},{"pres":42.1,"psal":32.503,"temp":4.853},{"pres":43,"psal":32.504,"temp":4.853},{"pres":44.1,"psal":32.504,"temp":4.852},{"pres":45,"psal":32.505,"temp":4.852},{"pres":45.9,"psal":32.505,"temp":4.852},{"pres":46.8,"psal":32.505,"temp":4.852},{"pres":47.8,"psal":32.507,"temp":4.85},{"pres":48.8,"psal":32.508,"temp":4.849},{"pres":49.8,"psal":32.507,"temp":4.849},{"pres":50.8,"psal":32.507,"temp":4.849},{"pres":51.8,"psal":32.508,"temp":4.849},{"pres":52.9,"psal":32.509,"temp":4.846},{"pres":53.9,"psal":32.509,"temp":4.846},{"pres":54.9,"psal":32.51,"temp":4.845},{"pres":56,"psal":32.511,"temp":4.844},{"pres":57,"psal":32.511,"temp":4.842},{"pres":58,"psal":32.512,"temp":4.839},{"pres":58.9,"psal":32.514,"temp":4.835},{"pres":60.1,"psal":32.515,"temp":4.833},{"pres":60.9,"psal":32.517,"temp":4.83},{"pres":62,"psal":32.518,"temp":4.825},{"pres":63,"psal":32.52,"temp":4.822},{"pres":64,"psal":32.521,"temp":4.819},{"pres":64.8,"psal":32.522,"temp":4.817},{"pres":66,"psal":32.525,"temp":4.809},{"pres":67.1,"psal":32.534,"temp":4.778},{"pres":68,"psal":32.544,"temp":4.739},{"pres":69,"psal":32.547,"temp":4.731},{"pres":69.9,"psal":32.548,"temp":4.729},{"pres":70.9,"psal":32.549,"temp":4.728},{"pres":71.9,"psal":32.55,"temp":4.712},{"pres":73.1,"psal":32.554,"temp":4.706},{"pres":74,"psal":32.558,"temp":4.692},{"pres":75,"psal":32.561,"temp":4.683},{"pres":76,"psal":32.566,"temp":4.671},{"pres":77.1,"psal":32.567,"temp":4.657},{"pres":78.1,"psal":32.571,"temp":4.621},{"pres":79,"psal":32.574,"temp":4.597},{"pres":79.8,"psal":32.576,"temp":4.581},{"pres":80.8,"psal":32.579,"temp":4.579},{"pres":81.8,"psal":32.592,"temp":4.627},{"pres":82.8,"psal":32.607,"temp":4.688},{"pres":83.7,"psal":32.636,"temp":4.737},{"pres":84.7,"psal":32.654,"temp":4.739},{"pres":86.1,"psal":32.663,"temp":4.733},{"pres":87.1,"psal":32.684,"temp":4.713},{"pres":88.1,"psal":32.698,"temp":4.718},{"pres":89.1,"psal":32.706,"temp":4.726},{"pres":90.1,"psal":32.789,"temp":4.756},{"pres":91,"psal":32.814,"temp":4.737},{"pres":91.8,"psal":32.893,"temp":4.696},{"pres":92.8,"psal":33.018,"temp":4.678},{"pres":93.8,"psal":33.176,"temp":4.611},{"pres":95,"psal":33.378,"temp":4.474},{"pres":95.9,"psal":33.404,"temp":4.461},{"pres":96.9,"psal":33.423,"temp":4.448},{"pres":97.9,"psal":33.436,"temp":4.44},{"pres":98.9,"psal":33.445,"temp":4.433},{"pres":99.9,"psal":33.447,"temp":4.431},{"pres":100.9,"psal":33.454,"temp":4.426},{"pres":102,"psal":33.46,"temp":4.418},{"pres":102.9,"psal":33.461,"temp":4.416},{"pres":104,"psal":33.46,"temp":4.414},{"pres":105,"psal":33.486,"temp":4.4},{"pres":106.1,"psal":33.496,"temp":4.399},{"pres":106.8,"psal":33.502,"temp":4.396},{"pres":107.9,"psal":33.523,"temp":4.387},{"pres":109.1,"psal":33.532,"temp":4.385},{"pres":109.9,"psal":33.539,"temp":4.382},{"pres":110.8,"psal":33.547,"temp":4.379},{"pres":111.6,"psal":33.553,"temp":4.376},{"pres":112.9,"psal":33.561,"temp":4.374},{"pres":114.2,"psal":33.57,"temp":4.374},{"pres":115.1,"psal":33.571,"temp":4.375},{"pres":116,"psal":33.572,"temp":4.375},{"pres":116.8,"psal":33.581,"temp":4.373},{"pres":117.7,"psal":33.587,"temp":4.371},{"pres":118.6,"psal":33.588,"temp":4.37},{"pres":119.9,"psal":33.601,"temp":4.369},{"pres":121.3,"psal":33.625,"temp":4.373},{"pres":122.2,"psal":33.631,"temp":4.371},{"pres":123.1,"psal":33.639,"temp":4.366},{"pres":124,"psal":33.658,"temp":4.347},{"pres":125,"psal":33.661,"temp":4.34},{"pres":125.9,"psal":33.681,"temp":4.332},{"pres":126.9,"psal":33.697,"temp":4.323},{"pres":127.9,"psal":33.697,"temp":4.322},{"pres":128.9,"psal":33.702,"temp":4.32},{"pres":129.9,"psal":33.709,"temp":4.319},{"pres":130.9,"psal":33.717,"temp":4.323},{"pres":131.9,"psal":33.724,"temp":4.321},{"pres":132.9,"psal":33.746,"temp":4.329},{"pres":133.9,"psal":33.748,"temp":4.33},{"pres":135,"psal":33.756,"temp":4.325},{"pres":136,"psal":33.764,"temp":4.314},{"pres":137,"psal":33.774,"temp":4.311},{"pres":138.1,"psal":33.778,"temp":4.31},{"pres":139.1,"psal":33.78,"temp":4.308},{"pres":140.1,"psal":33.781,"temp":4.307},{"pres":141.1,"psal":33.786,"temp":4.301},{"pres":142.1,"psal":33.787,"temp":4.3},{"pres":143.1,"psal":33.792,"temp":4.295},{"pres":144.1,"psal":33.795,"temp":4.291},{"pres":145.1,"psal":33.812,"temp":4.27},{"pres":146.1,"psal":33.82,"temp":4.257},{"pres":147.1,"psal":33.822,"temp":4.254},{"pres":148.1,"psal":33.825,"temp":4.25},{"pres":149.1,"psal":33.828,"temp":4.247},{"pres":150.2,"psal":33.828,"temp":4.245},{"pres":151.2,"psal":33.829,"temp":4.243},{"pres":152.2,"psal":33.83,"temp":4.241},{"pres":153.2,"psal":33.833,"temp":4.236},{"pres":154.1,"psal":33.833,"temp":4.234},{"pres":155.1,"psal":33.836,"temp":4.228},{"pres":156.1,"psal":33.84,"temp":4.221},{"pres":157.1,"psal":33.844,"temp":4.211},{"pres":158.1,"psal":33.846,"temp":4.207},{"pres":159,"psal":33.848,"temp":4.204},{"pres":160,"psal":33.848,"temp":4.203},{"pres":160.9,"psal":33.851,"temp":4.197},{"pres":161.9,"psal":33.854,"temp":4.192},{"pres":162.8,"psal":33.854,"temp":4.19},{"pres":163.8,"psal":33.856,"temp":4.186},{"pres":164.7,"psal":33.856,"temp":4.184},{"pres":165.7,"psal":33.858,"temp":4.182},{"pres":166.6,"psal":33.858,"temp":4.182},{"pres":167.6,"psal":33.859,"temp":4.179},{"pres":168.5,"psal":33.86,"temp":4.176},{"pres":169.5,"psal":33.862,"temp":4.173},{"pres":170.9,"psal":33.864,"temp":4.168},{"pres":172.4,"psal":33.868,"temp":4.162},{"pres":173.3,"psal":33.871,"temp":4.151},{"pres":174.3,"psal":33.874,"temp":4.147},{"pres":175.2,"psal":33.877,"temp":4.142},{"pres":176.2,"psal":33.88,"temp":4.138},{"pres":177.1,"psal":33.881,"temp":4.136},{"pres":178.1,"psal":33.882,"temp":4.134},{"pres":179.1,"psal":33.883,"temp":4.132},{"pres":180,"psal":33.883,"temp":4.131},{"pres":180.9,"psal":33.884,"temp":4.131},{"pres":181.8,"psal":33.885,"temp":4.128},{"pres":182.9,"psal":33.889,"temp":4.125},{"pres":184,"psal":33.891,"temp":4.124},{"pres":184.7,"psal":33.892,"temp":4.122},{"pres":185.8,"psal":33.893,"temp":4.12},{"pres":187,"psal":33.895,"temp":4.118},{"pres":187.8,"psal":33.896,"temp":4.117},{"pres":188.9,"psal":33.899,"temp":4.115},{"pres":190.1,"psal":33.902,"temp":4.111},{"pres":190.9,"psal":33.906,"temp":4.107},{"pres":191.7,"psal":33.906,"temp":4.108},{"pres":192.9,"psal":33.908,"temp":4.105},{"pres":194.1,"psal":33.909,"temp":4.104},{"pres":194.9,"psal":33.909,"temp":4.103},{"pres":195.7,"psal":33.911,"temp":4.101},{"pres":197,"psal":33.913,"temp":4.098},{"pres":198.2,"psal":33.918,"temp":4.093},{"pres":199,"psal":33.919,"temp":4.091},{"pres":199.9,"psal":33.921,"temp":4.089},{"pres":200.7,"psal":33.921,"temp":4.089},{"pres":202,"psal":33.922,"temp":4.086},{"pres":203.3,"psal":33.924,"temp":4.086},{"pres":204.2,"psal":33.926,"temp":4.083},{"pres":205.1,"psal":33.928,"temp":4.081},{"pres":206,"psal":33.929,"temp":4.08},{"pres":206.8,"psal":33.929,"temp":4.08},{"pres":207.7,"psal":33.929,"temp":4.08},{"pres":208.6,"psal":33.931,"temp":4.078},{"pres":209.6,"psal":33.932,"temp":4.076},{"pres":210.9,"psal":33.932,"temp":4.075},{"pres":212.3,"psal":33.933,"temp":4.074},{"pres":213.2,"psal":33.934,"temp":4.074},{"pres":214.2,"psal":33.934,"temp":4.073},{"pres":215.1,"psal":33.934,"temp":4.073},{"pres":216.1,"psal":33.934,"temp":4.072},{"pres":217,"psal":33.935,"temp":4.071},{"pres":218,"psal":33.937,"temp":4.07},{"pres":218.9,"psal":33.938,"temp":4.068},{"pres":219.9,"psal":33.937,"temp":4.068},{"pres":220.9,"psal":33.938,"temp":4.068},{"pres":221.8,"psal":33.937,"temp":4.068},{"pres":222.8,"psal":33.939,"temp":4.066},{"pres":223.8,"psal":33.94,"temp":4.064},{"pres":224.8,"psal":33.941,"temp":4.063},{"pres":225.8,"psal":33.941,"temp":4.062},{"pres":226.8,"psal":33.942,"temp":4.061},{"pres":227.8,"psal":33.945,"temp":4.058},{"pres":228.8,"psal":33.946,"temp":4.058},{"pres":229.8,"psal":33.947,"temp":4.057},{"pres":230.8,"psal":33.948,"temp":4.057},{"pres":231.8,"psal":33.949,"temp":4.057},{"pres":232.8,"psal":33.95,"temp":4.056},{"pres":233.8,"psal":33.95,"temp":4.055},{"pres":234.8,"psal":33.951,"temp":4.055},{"pres":235.8,"psal":33.951,"temp":4.054},{"pres":236.8,"psal":33.952,"temp":4.053},{"pres":237.8,"psal":33.952,"temp":4.053},{"pres":238.9,"psal":33.952,"temp":4.053},{"pres":239.9,"psal":33.953,"temp":4.052},{"pres":240.9,"psal":33.953,"temp":4.052},{"pres":241.9,"psal":33.956,"temp":4.049},{"pres":242.9,"psal":33.957,"temp":4.048},{"pres":244,"psal":33.957,"temp":4.048},{"pres":245,"psal":33.958,"temp":4.047},{"pres":246,"psal":33.959,"temp":4.046},{"pres":247,"psal":33.959,"temp":4.046},{"pres":248,"psal":33.96,"temp":4.046},{"pres":249,"psal":33.96,"temp":4.045},{"pres":250.1,"psal":33.961,"temp":4.044},{"pres":251.1,"psal":33.962,"temp":4.043},{"pres":252.1,"psal":33.962,"temp":4.043},{"pres":253.1,"psal":33.963,"temp":4.042},{"pres":254.1,"psal":33.964,"temp":4.04},{"pres":255,"psal":33.965,"temp":4.039},{"pres":256,"psal":33.966,"temp":4.039},{"pres":257,"psal":33.966,"temp":4.038},{"pres":258,"psal":33.967,"temp":4.038},{"pres":259,"psal":33.966,"temp":4.038},{"pres":260,"psal":33.967,"temp":4.037},{"pres":261,"psal":33.968,"temp":4.036},{"pres":262,"psal":33.969,"temp":4.034},{"pres":263,"psal":33.97,"temp":4.033},{"pres":264,"psal":33.971,"temp":4.031},{"pres":265,"psal":33.974,"temp":4.029},{"pres":266,"psal":33.975,"temp":4.028},{"pres":267,"psal":33.979,"temp":4.024},{"pres":267.9,"psal":33.978,"temp":4.025},{"pres":268.9,"psal":33.979,"temp":4.024},{"pres":269.9,"psal":33.979,"temp":4.024},{"pres":270.8,"psal":33.979,"temp":4.024},{"pres":271.8,"psal":33.981,"temp":4.022},{"pres":272.8,"psal":33.982,"temp":4.021},{"pres":273.7,"psal":33.982,"temp":4.02},{"pres":274.7,"psal":33.983,"temp":4.02},{"pres":275.7,"psal":33.983,"temp":4.019},{"pres":276.7,"psal":33.983,"temp":4.019},{"pres":277.6,"psal":33.984,"temp":4.018},{"pres":278.6,"psal":33.984,"temp":4.018},{"pres":279.5,"psal":33.985,"temp":4.018},{"pres":280.5,"psal":33.984,"temp":4.018},{"pres":281.9,"psal":33.985,"temp":4.017},{"pres":283.4,"psal":33.986,"temp":4.016},{"pres":284.4,"psal":33.986,"temp":4.015},{"pres":285.3,"psal":33.987,"temp":4.014},{"pres":286.3,"psal":33.989,"temp":4.013},{"pres":287.2,"psal":33.99,"temp":4.011},{"pres":288.2,"psal":33.991,"temp":4.01},{"pres":289.1,"psal":33.992,"temp":4.009},{"pres":290,"psal":33.992,"temp":4.01},{"pres":291,"psal":33.994,"temp":4.008},{"pres":291.9,"psal":33.994,"temp":4.008},{"pres":292.9,"psal":33.995,"temp":4.006},{"pres":293.8,"psal":33.997,"temp":4.004},{"pres":294.8,"psal":33.999,"temp":3.997},{"pres":295.7,"psal":34.002,"temp":3.995},{"pres":296.7,"psal":34.002,"temp":3.994},{"pres":297.6,"psal":34.003,"temp":3.994},{"pres":298.6,"psal":34.003,"temp":3.993},{"pres":299.5,"psal":34.005,"temp":3.991},{"pres":300.9,"psal":34.009,"temp":3.987},{"pres":302.4,"psal":34.012,"temp":3.984},{"pres":303.3,"psal":34.011,"temp":3.984},{"pres":304.2,"psal":34.012,"temp":3.984},{"pres":305.2,"psal":34.012,"temp":3.984},{"pres":306.1,"psal":34.012,"temp":3.984},{"pres":307.1,"psal":34.012,"temp":3.984},{"pres":308,"psal":34.014,"temp":3.982},{"pres":309,"psal":34.014,"temp":3.981},{"pres":310,"psal":34.015,"temp":3.981},{"pres":310.9,"psal":34.015,"temp":3.981},{"pres":311.9,"psal":34.016,"temp":3.979},{"pres":312.8,"psal":34.018,"temp":3.979},{"pres":313.8,"psal":34.018,"temp":3.978},{"pres":314.8,"psal":34.018,"temp":3.978},{"pres":315.7,"psal":34.02,"temp":3.975},{"pres":316.7,"psal":34.024,"temp":3.969},{"pres":317.7,"psal":34.024,"temp":3.969},{"pres":318.7,"psal":34.024,"temp":3.969},{"pres":319.6,"psal":34.025,"temp":3.968},{"pres":320.6,"psal":34.025,"temp":3.968},{"pres":321.5,"psal":34.026,"temp":3.967},{"pres":322.5,"psal":34.026,"temp":3.966},{"pres":323.5,"psal":34.028,"temp":3.964},{"pres":324.5,"psal":34.029,"temp":3.962},{"pres":325.9,"psal":34.035,"temp":3.954},{"pres":327.4,"psal":34.036,"temp":3.952},{"pres":328.4,"psal":34.036,"temp":3.952},{"pres":329.4,"psal":34.037,"temp":3.952},{"pres":330.4,"psal":34.038,"temp":3.951},{"pres":331.4,"psal":34.04,"temp":3.949},{"pres":332.3,"psal":34.04,"temp":3.949},{"pres":333.4,"psal":34.04,"temp":3.948},{"pres":334.3,"psal":34.043,"temp":3.945},{"pres":335.4,"psal":34.044,"temp":3.943},{"pres":336.4,"psal":34.044,"temp":3.942},{"pres":337.4,"psal":34.046,"temp":3.94},{"pres":338.4,"psal":34.047,"temp":3.938},{"pres":339.4,"psal":34.048,"temp":3.937},{"pres":340.4,"psal":34.048,"temp":3.937},{"pres":341.4,"psal":34.05,"temp":3.935},{"pres":342.5,"psal":34.05,"temp":3.935},{"pres":343.5,"psal":34.051,"temp":3.934},{"pres":344.5,"psal":34.054,"temp":3.931},{"pres":345.6,"psal":34.055,"temp":3.93},{"pres":346.6,"psal":34.058,"temp":3.926},{"pres":347.6,"psal":34.06,"temp":3.922},{"pres":348.7,"psal":34.06,"temp":3.922},{"pres":349.7,"psal":34.062,"temp":3.92},{"pres":350.7,"psal":34.062,"temp":3.92},{"pres":351.6,"psal":34.066,"temp":3.916},{"pres":353,"psal":34.066,"temp":3.913},{"pres":354.2,"psal":34.066,"temp":3.913},{"pres":355,"psal":34.067,"temp":3.913},{"pres":355.8,"psal":34.066,"temp":3.913},{"pres":357,"psal":34.066,"temp":3.912},{"pres":358.1,"psal":34.067,"temp":3.912},{"pres":358.9,"psal":34.067,"temp":3.911},{"pres":359.7,"psal":34.066,"temp":3.912},{"pres":360.9,"psal":34.07,"temp":3.904},{"pres":362.1,"psal":34.072,"temp":3.9},{"pres":362.9,"psal":34.072,"temp":3.899},{"pres":363.7,"psal":34.073,"temp":3.898},{"pres":365,"psal":34.073,"temp":3.897},{"pres":366.2,"psal":34.073,"temp":3.897},{"pres":367,"psal":34.076,"temp":3.893},{"pres":367.8,"psal":34.076,"temp":3.893},{"pres":368.6,"psal":34.077,"temp":3.892},{"pres":369.9,"psal":34.076,"temp":3.891},{"pres":371.1,"psal":34.077,"temp":3.892},{"pres":371.9,"psal":34.078,"temp":3.891},{"pres":372.7,"psal":34.078,"temp":3.891},{"pres":373.9,"psal":34.078,"temp":3.89},{"pres":375.2,"psal":34.08,"temp":3.889},{"pres":376,"psal":34.082,"temp":3.887},{"pres":376.9,"psal":34.082,"temp":3.887},{"pres":377.7,"psal":34.082,"temp":3.887},{"pres":378.9,"psal":34.082,"temp":3.887},{"pres":380.2,"psal":34.083,"temp":3.888},{"pres":381,"psal":34.083,"temp":3.888},{"pres":381.8,"psal":34.085,"temp":3.886},{"pres":382.7,"psal":34.086,"temp":3.884},{"pres":383.9,"psal":34.09,"temp":3.875},{"pres":385.2,"psal":34.094,"temp":3.869},{"pres":386,"psal":34.095,"temp":3.866},{"pres":386.8,"psal":34.097,"temp":3.864},{"pres":387.7,"psal":34.097,"temp":3.863},{"pres":388.9,"psal":34.098,"temp":3.861},{"pres":390.2,"psal":34.1,"temp":3.857},{"pres":391,"psal":34.103,"temp":3.85},{"pres":391.9,"psal":34.104,"temp":3.846},{"pres":392.7,"psal":34.104,"temp":3.845},{"pres":393.9,"psal":34.104,"temp":3.845},{"pres":395.2,"psal":34.105,"temp":3.844},{"pres":396.1,"psal":34.105,"temp":3.844},{"pres":396.9,"psal":34.105,"temp":3.843},{"pres":397.7,"psal":34.105,"temp":3.843},{"pres":399,"psal":34.105,"temp":3.838},{"pres":400.2,"psal":34.108,"temp":3.831},{"pres":401.1,"psal":34.11,"temp":3.831},{"pres":401.9,"psal":34.111,"temp":3.83},{"pres":402.8,"psal":34.111,"temp":3.83},{"pres":403.6,"psal":34.111,"temp":3.83},{"pres":404.9,"psal":34.111,"temp":3.829},{"pres":406.2,"psal":34.115,"temp":3.826},{"pres":407.1,"psal":34.116,"temp":3.823},{"pres":407.9,"psal":34.116,"temp":3.821},{"pres":408.8,"psal":34.117,"temp":3.819},{"pres":409.7,"psal":34.118,"temp":3.817},{"pres":410.9,"psal":34.118,"temp":3.817},{"pres":412.2,"psal":34.119,"temp":3.815},{"pres":413.1,"psal":34.119,"temp":3.815},{"pres":413.9,"psal":34.12,"temp":3.814},{"pres":414.8,"psal":34.12,"temp":3.814},{"pres":415.6,"psal":34.121,"temp":3.813},{"pres":416.9,"psal":34.122,"temp":3.81},{"pres":418.2,"psal":34.123,"temp":3.809},{"pres":419.1,"psal":34.123,"temp":3.808},{"pres":419.9,"psal":34.123,"temp":3.808},{"pres":420.8,"psal":34.124,"temp":3.806},{"pres":421.6,"psal":34.124,"temp":3.807},{"pres":422.9,"psal":34.126,"temp":3.804},{"pres":424.2,"psal":34.127,"temp":3.801},{"pres":425.1,"psal":34.127,"temp":3.801},{"pres":425.9,"psal":34.127,"temp":3.801},{"pres":426.7,"psal":34.128,"temp":3.8},{"pres":427.6,"psal":34.128,"temp":3.8},{"pres":428.9,"psal":34.128,"temp":3.798},{"pres":430.2,"psal":34.129,"temp":3.793},{"pres":431.1,"psal":34.129,"temp":3.792},{"pres":431.9,"psal":34.13,"temp":3.791},{"pres":432.8,"psal":34.131,"temp":3.79},{"pres":433.7,"psal":34.131,"temp":3.79},{"pres":434.9,"psal":34.131,"temp":3.786},{"pres":436.3,"psal":34.133,"temp":3.785},{"pres":437.2,"psal":34.132,"temp":3.785},{"pres":438.1,"psal":34.133,"temp":3.785},{"pres":438.9,"psal":34.133,"temp":3.785},{"pres":439.8,"psal":34.135,"temp":3.781},{"pres":440.7,"psal":34.135,"temp":3.781},{"pres":441.6,"psal":34.135,"temp":3.78},{"pres":442.9,"psal":34.136,"temp":3.776},{"pres":444.2,"psal":34.138,"temp":3.772},{"pres":445.1,"psal":34.139,"temp":3.77},{"pres":446,"psal":34.139,"temp":3.768},{"pres":446.9,"psal":34.14,"temp":3.766},{"pres":447.8,"psal":34.14,"temp":3.765},{"pres":448.7,"psal":34.141,"temp":3.763},{"pres":449.6,"psal":34.141,"temp":3.76},{"pres":450.9,"psal":34.142,"temp":3.751},{"pres":452.3,"psal":34.144,"temp":3.749},{"pres":453.2,"psal":34.143,"temp":3.748},{"pres":454.1,"psal":34.144,"temp":3.747},{"pres":455,"psal":34.144,"temp":3.747},{"pres":455.9,"psal":34.144,"temp":3.747},{"pres":456.8,"psal":34.144,"temp":3.746},{"pres":457.7,"psal":34.145,"temp":3.744},{"pres":458.6,"psal":34.146,"temp":3.741},{"pres":459.9,"psal":34.149,"temp":3.734},{"pres":461.3,"psal":34.15,"temp":3.731},{"pres":462.2,"psal":34.151,"temp":3.73},{"pres":463.1,"psal":34.151,"temp":3.728},{"pres":464,"psal":34.152,"temp":3.727},{"pres":464.9,"psal":34.153,"temp":3.725},{"pres":465.8,"psal":34.153,"temp":3.723},{"pres":466.7,"psal":34.153,"temp":3.722},{"pres":467.6,"psal":34.154,"temp":3.721},{"pres":468.9,"psal":34.155,"temp":3.72},{"pres":470.2,"psal":34.156,"temp":3.718},{"pres":471.2,"psal":34.157,"temp":3.715},{"pres":472.1,"psal":34.157,"temp":3.713},{"pres":472.9,"psal":34.158,"temp":3.711},{"pres":473.8,"psal":34.159,"temp":3.71},{"pres":474.8,"psal":34.159,"temp":3.709},{"pres":477.9,"psal":34.162,"temp":3.7},{"pres":482.9,"psal":34.166,"temp":3.69},{"pres":488,"psal":34.169,"temp":3.681},{"pres":493.3,"psal":34.173,"temp":3.665},{"pres":498.1,"psal":34.179,"temp":3.646},{"pres":503.1,"psal":34.179,"temp":3.644},{"pres":508.1,"psal":34.182,"temp":3.633},{"pres":513.2,"psal":34.188,"temp":3.618},{"pres":517.9,"psal":34.192,"temp":3.606},{"pres":522.8,"psal":34.196,"temp":3.602},{"pres":527.8,"psal":34.198,"temp":3.594},{"pres":532.8,"psal":34.201,"temp":3.585},{"pres":537.8,"psal":34.202,"temp":3.582},{"pres":543,"psal":34.203,"temp":3.579},{"pres":547.8,"psal":34.205,"temp":3.571},{"pres":552.7,"psal":34.207,"temp":3.564},{"pres":558.1,"psal":34.209,"temp":3.559},{"pres":563,"psal":34.211,"temp":3.556},{"pres":567.9,"psal":34.214,"temp":3.545},{"pres":573,"psal":34.215,"temp":3.54},{"pres":577.7,"psal":34.219,"temp":3.527},{"pres":582.9,"psal":34.222,"temp":3.517},{"pres":588.2,"psal":34.226,"temp":3.503},{"pres":592.9,"psal":34.23,"temp":3.488},{"pres":597.7,"psal":34.231,"temp":3.483},{"pres":603,"psal":34.234,"temp":3.474},{"pres":608.3,"psal":34.235,"temp":3.467},{"pres":613.2,"psal":34.236,"temp":3.46},{"pres":618.2,"psal":34.239,"temp":3.453},{"pres":623.2,"psal":34.241,"temp":3.444},{"pres":628.2,"psal":34.244,"temp":3.435},{"pres":633.3,"psal":34.247,"temp":3.425},{"pres":638.4,"psal":34.251,"temp":3.415},{"pres":643.4,"psal":34.253,"temp":3.406},{"pres":648.4,"psal":34.257,"temp":3.394},{"pres":653.3,"psal":34.26,"temp":3.385},{"pres":658.3,"psal":34.263,"temp":3.374},{"pres":663.2,"psal":34.265,"temp":3.367},{"pres":668.2,"psal":34.268,"temp":3.357},{"pres":673.1,"psal":34.269,"temp":3.348},{"pres":678.1,"psal":34.271,"temp":3.341},{"pres":683.1,"psal":34.274,"temp":3.332},{"pres":688.1,"psal":34.276,"temp":3.326},{"pres":693.1,"psal":34.278,"temp":3.314},{"pres":698.2,"psal":34.282,"temp":3.297},{"pres":703.3,"psal":34.285,"temp":3.286},{"pres":707.9,"psal":34.287,"temp":3.278},{"pres":712.6,"psal":34.288,"temp":3.273},{"pres":717.9,"psal":34.29,"temp":3.256},{"pres":722.8,"psal":34.293,"temp":3.238},{"pres":728,"psal":34.296,"temp":3.224},{"pres":733.2,"psal":34.299,"temp":3.211},{"pres":738.2,"psal":34.302,"temp":3.202},{"pres":743.1,"psal":34.304,"temp":3.197},{"pres":748.2,"psal":34.306,"temp":3.189},{"pres":753.2,"psal":34.308,"temp":3.184},{"pres":758.3,"psal":34.309,"temp":3.177},{"pres":762.9,"psal":34.311,"temp":3.167},{"pres":767.7,"psal":34.313,"temp":3.155},{"pres":772.8,"psal":34.316,"temp":3.144},{"pres":778.1,"psal":34.319,"temp":3.133},{"pres":783,"psal":34.32,"temp":3.128},{"pres":787.9,"psal":34.323,"temp":3.119},{"pres":792.9,"psal":34.325,"temp":3.107},{"pres":798,"psal":34.328,"temp":3.096},{"pres":803.3,"psal":34.331,"temp":3.081},{"pres":808.2,"psal":34.333,"temp":3.069},{"pres":813.1,"psal":34.334,"temp":3.063},{"pres":818.1,"psal":34.337,"temp":3.054},{"pres":823.1,"psal":34.338,"temp":3.049},{"pres":828.1,"psal":34.339,"temp":3.044},{"pres":833.2,"psal":34.342,"temp":3.033},{"pres":838.3,"psal":34.344,"temp":3.024},{"pres":842.9,"psal":34.345,"temp":3.019},{"pres":847.5,"psal":34.347,"temp":3.009},{"pres":852.6,"psal":34.349,"temp":3.002},{"pres":857.8,"psal":34.35,"temp":2.995},{"pres":863,"psal":34.352,"temp":2.989},{"pres":868.2,"psal":34.352,"temp":2.986},{"pres":872.8,"psal":34.353,"temp":2.984},{"pres":877.5,"psal":34.354,"temp":2.98},{"pres":883.1,"psal":34.356,"temp":2.969},{"pres":888.2,"psal":34.358,"temp":2.96},{"pres":893,"psal":34.36,"temp":2.95},{"pres":897.8,"psal":34.362,"temp":2.943},{"pres":902.8,"psal":34.363,"temp":2.936},{"pres":907.8,"psal":34.365,"temp":2.928},{"pres":912.8,"psal":34.366,"temp":2.924},{"pres":917.9,"psal":34.367,"temp":2.917},{"pres":923,"psal":34.37,"temp":2.905},{"pres":928.2,"psal":34.371,"temp":2.896},{"pres":933,"psal":34.372,"temp":2.891},{"pres":937.9,"psal":34.374,"temp":2.882},{"pres":943.2,"psal":34.376,"temp":2.878},{"pres":948,"psal":34.376,"temp":2.876},{"pres":952.8,"psal":34.376,"temp":2.874},{"pres":958,"psal":34.379,"temp":2.864},{"pres":962.8,"psal":34.381,"temp":2.856},{"pres":967.7,"psal":34.381,"temp":2.852},{"pres":973,"psal":34.383,"temp":2.843},{"pres":977.8,"psal":34.385,"temp":2.833},{"pres":982.7,"psal":34.387,"temp":2.825},{"pres":988.1,"psal":34.39,"temp":2.813},{"pres":992.9,"psal":34.391,"temp":2.808},{"pres":997.8,"psal":34.392,"temp":2.802},{"pres":1005.4,"psal":34.394,"temp":2.79},{"pres":1015.3,"psal":34.397,"temp":2.774},{"pres":1025.6,"psal":34.402,"temp":2.755},{"pres":1035.6,"psal":34.404,"temp":2.744},{"pres":1045.3,"psal":34.407,"temp":2.727},{"pres":1055.2,"psal":34.41,"temp":2.715},{"pres":1065.3,"psal":34.412,"temp":2.703},{"pres":1075.6,"psal":34.414,"temp":2.694},{"pres":1085.6,"psal":34.417,"temp":2.68},{"pres":1095.2,"psal":34.42,"temp":2.663},{"pres":1105.4,"psal":34.424,"temp":2.645},{"pres":1115.8,"psal":34.428,"temp":2.623},{"pres":1125.2,"psal":34.431,"temp":2.611},{"pres":1135.1,"psal":34.432,"temp":2.601},{"pres":1145.4,"psal":34.436,"temp":2.585},{"pres":1155.4,"psal":34.44,"temp":2.569},{"pres":1165.1,"psal":34.443,"temp":2.55},{"pres":1175.5,"psal":34.447,"temp":2.532},{"pres":1185.7,"psal":34.451,"temp":2.511},{"pres":1195.5,"psal":34.453,"temp":2.501},{"pres":1205.4,"psal":34.455,"temp":2.49},{"pres":1215.3,"psal":34.457,"temp":2.477},{"pres":1225.3,"psal":34.461,"temp":2.461},{"pres":1235.4,"psal":34.463,"temp":2.45},{"pres":1245.4,"psal":34.466,"temp":2.439},{"pres":1255.4,"psal":34.468,"temp":2.427},{"pres":1265.5,"psal":34.471,"temp":2.41},{"pres":1275.6,"psal":34.474,"temp":2.396},{"pres":1285.6,"psal":34.476,"temp":2.385},{"pres":1295.6,"psal":34.479,"temp":2.373},{"pres":1305.4,"psal":34.482,"temp":2.361},{"pres":1315.2,"psal":34.484,"temp":2.348},{"pres":1325.2,"psal":34.488,"temp":2.33},{"pres":1335.5,"psal":34.49,"temp":2.323},{"pres":1345.5,"psal":34.492,"temp":2.314},{"pres":1355.4,"psal":34.493,"temp":2.309},{"pres":1365.3,"psal":34.495,"temp":2.301},{"pres":1375.6,"psal":34.498,"temp":2.286},{"pres":1385.2,"psal":34.5,"temp":2.274},{"pres":1395.3,"psal":34.504,"temp":2.259},{"pres":1405.4,"psal":34.505,"temp":2.25},{"pres":1415.4,"psal":34.508,"temp":2.238},{"pres":1425.3,"psal":34.511,"temp":2.223},{"pres":1435.2,"psal":34.513,"temp":2.214},{"pres":1445.3,"psal":34.516,"temp":2.2},{"pres":1455.3,"psal":34.517,"temp":2.195},{"pres":1465.5,"psal":34.519,"temp":2.189},{"pres":1475.3,"psal":34.52,"temp":2.184},{"pres":1485.6,"psal":34.522,"temp":2.173},{"pres":1495.8,"psal":34.524,"temp":2.164},{"pres":1505.4,"psal":34.525,"temp":2.158},{"pres":1515.4,"psal":34.526,"temp":2.153},{"pres":1525.4,"psal":34.528,"temp":2.147},{"pres":1535.5,"psal":34.529,"temp":2.141},{"pres":1545.5,"psal":34.531,"temp":2.134},{"pres":1555.6,"psal":34.532,"temp":2.128},{"pres":1565.8,"psal":34.533,"temp":2.123},{"pres":1575.7,"psal":34.534,"temp":2.119},{"pres":1585.7,"psal":34.535,"temp":2.116},{"pres":1595.7,"psal":34.536,"temp":2.108},{"pres":1605.7,"psal":34.538,"temp":2.099},{"pres":1615.7,"psal":34.54,"temp":2.094},{"pres":1625.6,"psal":34.54,"temp":2.09},{"pres":1635.5,"psal":34.542,"temp":2.083},{"pres":1645.2,"psal":34.543,"temp":2.077},{"pres":1655.3,"psal":34.544,"temp":2.07},{"pres":1665.5,"psal":34.546,"temp":2.063},{"pres":1675.2,"psal":34.548,"temp":2.056},{"pres":1685.1,"psal":34.549,"temp":2.048},{"pres":1695.1,"psal":34.551,"temp":2.041},{"pres":1705,"psal":34.552,"temp":2.035},{"pres":1714.9,"psal":34.554,"temp":2.026},{"pres":1725,"psal":34.555,"temp":2.02},{"pres":1735.5,"psal":34.556,"temp":2.014},{"pres":1745.7,"psal":34.558,"temp":2.007},{"pres":1755.6,"psal":34.559,"temp":2.001},{"pres":1765.3,"psal":34.561,"temp":1.994},{"pres":1775.2,"psal":34.561,"temp":1.989},{"pres":1785.5,"psal":34.562,"temp":1.986},{"pres":1795.5,"psal":34.563,"temp":1.982},{"pres":1805.3,"psal":34.564,"temp":1.979},{"pres":1815.2,"psal":34.565,"temp":1.973},{"pres":1825.2,"psal":34.566,"temp":1.968},{"pres":1835.3,"psal":34.568,"temp":1.961},{"pres":1845.6,"psal":34.569,"temp":1.956},{"pres":1855.6,"psal":34.57,"temp":1.951},{"pres":1865.2,"psal":34.571,"temp":1.946},{"pres":1875.4,"psal":34.572,"temp":1.94},{"pres":1885.7,"psal":34.574,"temp":1.929},{"pres":1895.7,"psal":34.576,"temp":1.922},{"pres":1905.3,"psal":34.577,"temp":1.917},{"pres":1915.6,"psal":34.579,"temp":1.91},{"pres":1925.7,"psal":34.579,"temp":1.906},{"pres":1935.7,"psal":34.581,"temp":1.9},{"pres":1945.5,"psal":34.582,"temp":1.895},{"pres":1955.4,"psal":34.582,"temp":1.891},{"pres":1965.6,"psal":34.584,"temp":1.886},{"pres":1975.4,"psal":34.585,"temp":1.88},{"pres":1985.3,"psal":34.586,"temp":1.875},{"pres":1995.3,"psal":34.587,"temp":1.871},{"pres":2005.6,"psal":34.588,"temp":1.863},{"pres":2013.3,"psal":34.589,"temp":1.858}],"station_parameters":["pres","psal","temp"],"pres_max_for_TEMP":2013.3,"pres_min_for_TEMP":3.4,"pres_max_for_PSAL":2013.3,"pres_min_for_PSAL":3.4,"max_pres":2013.3,"date":"2023-01-26T05:19:00.000Z","date_added":"2023-01-27T08:08:38.602Z","date_qc":1,"lat":54.25397491455078,"lon":-151.1851043701172,"geoLocation":{"type":"Point","coordinates":[-151.1851043701172,54.25397491455078]},"position_qc":1,"cycle_number":56,"dac":"meds","platform_number":4902521,"station_parameters_in_nc":["MTIME","PRES","PSAL","TEMP"],"nc_url":"ftp://ftp.ifremer.fr/ifremer/argo/dac/meds/4902521/profiles/R4902521_056.nc","DIRECTION":"A","BASIN":2,"core_data_mode":"R","roundLat":"54.254","roundLon":"-151.185","strLat":"54.254 N","strLon":"151.185 W","formatted_station_parameters":[" pres"," psal"," temp"]}] \ No newline at end of file diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index e0c2663f3..0f47fee61 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -1,4 +1,5 @@ import icepyx as ipx +import json import pytest import warnings from icepyx.quest.dataset_scripts.argo import Argo @@ -48,11 +49,21 @@ def test_fmt_coordinates(): def test_parse_into_df(): - reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + reg_a = Argo([-154, 54, -151, 56], ["2023-01-20", "2023-01-29"]) reg_a.search_data() + obs_df = reg_a.profiles + + exp = 0 + with open("./icepyx/tests/argovis_test_data2.json") as file: + data = json.load(file) + for profile in data: + exp = exp + len(profile["measurements"]) - pass + assert exp == len(obs_df) # goal: check number of rows in df matches rows in json # approach: create json files with profiles and store them in test suite # then use those for the comparison + # update: for some reason the file downloaded from argovis and the one created here had different lengths + # by downloading the json from the url of the search here, they matched + # next steps: testing argo bgc? From 8dfb33e85565db4e757751386439a9a1855c4361 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 22 May 2023 15:30:44 -0400 Subject: [PATCH 039/124] update argo to submit successful api request (update keys and values submitted) --- icepyx/quest/dataset_scripts/argo.py | 82 +++++++++++++++------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 9fb18760e..a06661d33 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -8,36 +8,37 @@ class Argo(DataSet): """ - Initialises an Argo Dataset object - Used to query physical Argo profiles - -> biogeochemical Argo (BGC) not included + Initialises an Argo Dataset object + Used to query physical Argo profiles + -> biogeochemical Argo (BGC) not included - Examples + Examples -------- # example with profiles available >>> reg_a = Argo([-154, 30,-143, 37], ['2022-04-12', '2022-04-26']) - >>> reg_a.search_data() - >>> print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) - pres temp lat lon - 0 3.9 18.608 33.401 -153.913 - 1 5.7 18.598 33.401 -153.913 - 2 7.7 18.588 33.401 -153.913 - 3 9.7 18.462 33.401 -153.913 - 4 11.7 18.378 33.401 -153.913 - - # example with no profiles - >>> reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) - >>> reg_a.search_data() - Warning: Query returned no profiles - Please try different search parameters - - - See Also - -------- - DataSet - GenQuery + >>> reg_a.search_data() + >>> print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) + pres temp lat lon + 0 3.9 18.608 33.401 -153.913 + 1 5.7 18.598 33.401 -153.913 + 2 7.7 18.588 33.401 -153.913 + 3 9.7 18.462 33.401 -153.913 + 4 11.7 18.378 33.401 -153.913 + + # example with no profiles + >>> reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) + >>> reg_a.search_data() + Warning: Query returned no profiles + Please try different search parameters + + + See Also + -------- + DataSet + GenQuery """ + # DevNote: it looks like ArgoVis now accepts polygons, not just bounding boxes def __init__(self, boundingbox, timeframe): super().__init__(boundingbox, timeframe) assert self._spatial._ext_type == "bounding_box" @@ -50,17 +51,18 @@ def search_data(self, presRange=None, printURL=False) -> str: """ # builds URL to be submitted - baseURL = "https://argovis.colorado.edu/selection/profiles/" + baseURL = "https://argovis-api.colorado.edu/argo" payload = { - "startDate": self._start.strftime("%Y-%m-%d"), - "endDate": self._end.strftime("%Y-%m-%d"), - "shape": [self._fmt_coordinates()], + "startDate": self._temporal._start.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "endDate": self._temporal._end.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "polygon": [self._fmt_coordinates()], } if presRange: payload["presRange"] = presRange # submit request - resp = requests.get(baseURL, params=payload) + apikey = "92259861231b55d32a9c0e4e3a93f4834fc0b6fa" + resp = requests.get(baseURL, headers={"x-argokey": apikey}, params=payload) if printURL: print(resp.url) @@ -103,7 +105,7 @@ def _fmt_coordinates(self) -> str: else: x += "," + coord - x = "[[" + x + "]]" + x = "[" + x + "]" return x # todo: add a try/except to make sure the json files are valid i.e. contains all data we're expecting (no params are missing) @@ -111,17 +113,23 @@ def _parse_into_df(self, profiles) -> None: """ Stores profiles returned by query into dataframe saves profiles back to self.profiles - returns None + + Returns + ------- + None """ # initialize dict df = pd.DataFrame() for profile in profiles: - profileDf = pd.DataFrame(profile["measurements"]) - profileDf["cycle_number"] = profile["cycle_number"] + # this line "works", but data is no longer included in the response so the resulting df is useless + profileDf = pd.DataFrame(profile["data_info"]) + # Note: the cycle_number is returned as part of the id: _ + # profileDf["cycle_number"] = profile["cycle_number"] profileDf["profile_id"] = profile["_id"] - profileDf["lat"] = profile["lat"] - profileDf["lon"] = profile["lon"] - profileDf["date"] = profile["date"] + # there's also a geolocation field that provides the geospatial info as shapely points + profileDf["lat"] = profile["geolocation"]["coordinates"][1] + profileDf["lon"] = profile["geolocation"]["coordinates"][0] + profileDf["date"] = profile["timestamp"] df = pd.concat([df, profileDf], sort=False) self.profiles = df @@ -132,5 +140,5 @@ def _parse_into_df(self, profiles) -> None: # reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) # profiles available reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) - reg_a.search_data() + reg_a.search_data(printURL=True) print(reg_a.profiles[["pres", "temp", "lat", "lon"]].head()) From d43da757b5e98d67818ee87a104d3ed43dd3d637 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Tue, 30 May 2023 17:36:28 -0400 Subject: [PATCH 040/124] first pass at porting argo over to metadata+per profile download (WIP) --- icepyx/quest/dataset_scripts/BGCargo.py | 412 ++++++++++++------------ icepyx/quest/dataset_scripts/argo.py | 192 +++++++++-- 2 files changed, 378 insertions(+), 226 deletions(-) diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py index 8dc3f4eb3..d498da867 100644 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ b/icepyx/quest/dataset_scripts/BGCargo.py @@ -8,209 +8,209 @@ class BGC_Argo(Argo): - def __init__(self, boundingbox, timeframe): - super().__init__(boundingbox, timeframe) - # self.profiles = None - - def _search_data_BGC_helper(self): - ''' - make request with two params, and identify profiles that contain - remaining params - i.e. mandates the intersection of all specified params - ''' - pass - - def search_data(self, params, presRange=None, printURL=False, keep_all=True): - - assert len(params) != 0, 'One or more BGC measurements must be specified.' - - # API request requires exactly 2 measurement params, duplicate single of necessary - if len(params) == 1: - params.append(params[0]) - - # validate list of user-entered params, sorts into order to be queried - params = self._validate_parameters(params) - - - # builds URL to be submitted - baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection/' - - payload = {'startDate': self._start.strftime('%Y-%m-%d'), - 'endDate': self._end.strftime('%Y-%m-%d'), - 'shape': [self._fmt_coordinates()], - 'meas_1':params[0], - 'meas_2':params[1]} - - if presRange: - payload['presRange'] = presRange - - # submit request - resp = requests.get(baseURL, params=payload) - - if printURL: - print(resp.url) - - # Consider any status other than 2xx an error - if not resp.status_code // 100 == 2: - msg = "Error: Unexpected response {}".format(resp) - print(msg) - return - - selectionProfiles = resp.json() - - # check for the existence of profiles from query - if selectionProfiles == []: - msg = 'Warning: Query returned no profiles\n' \ - 'Please try different search parameters' - print(msg) - return - - - # deterine which profiles contain all specified params - prof_ids = self._filter_profiles(selectionProfiles, params) - - print('{0} valid profiles have been identified'.format(len(prof_ids))) - # iterate and download profiles individually - for i in prof_ids: - print("processing profile", i) - self.download_by_profile(i) - - self.profiles.reset_index(inplace=True) - - if not keep_all: - # drop BGC measurement columns not specified by user - drop_params = list(set(list(self._valid_BGC_params())[3:]) - set(params)) - qc_params = [] - for i in drop_params: - qc_params.append(i + '_qc') - drop_params += qc_params - self.profiles.drop(columns=drop_params, inplace=True, errors='ignore') - - def _valid_BGC_params(self): - ''' - This is a list of valid BGC params, stored here to remove redundancy - They are ordered by how commonly they are measured (approx) - ''' - params = valid_params = { - 'pres':0, - 'temp':1, - 'psal':2, - 'cndx':3, - 'doxy':4, - 'ph_in_situ_total':5, - 'chla':6, - 'cdom':7, - 'nitrate':8, - 'bbp700':9, - 'down_irradiance412':10, - 'down_irradiance442':11, - 'down_irradiance490':12, - 'down_irradiance380': 13, - 'downwelling_par':14, - } - return params - - def _validate_parameters(self, params): - ''' - Asserts that user-specified parameters are valid as per the Argovis documentation here: - https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_ - - Returns - ------- - the list of params sorted in the order in which they should be queried (least - commonly available to most commonly available) - ''' - - # valid params ordered by how commonly they are measured (approx) - valid_params = self._valid_BGC_params() - - # checks that params are valid - for i in params: - assert i in valid_params.keys(), \ - "Parameter '{0}' is not valid. Valid parameters are {1}".format(i, valid_params.keys()) - - # sorts params into order in which they should be queried - params = sorted(params, key= lambda i: valid_params[i], reverse=True) - return params - - def _filter_profiles(self, profiles, params): - ''' - from a dictionary of all profiles returned by first API request, remove the - profiles that do not contain ALL BGC measurements specified by user - returns a list of profile ID's that contain all necessary BGC params - ''' - # todo: filter out BGC profiles - good_profs = [] - for i in profiles: - bgc_meas = i['bgcMeasKeys'] - check = all(item in bgc_meas for item in params) - if check: - good_profs.append(i['_id']) - # print(i['_id']) - - # profiles = good_profs - return good_profs - - def download_by_profile(self, profile_number): - url = 'https://argovis.colorado.edu/catalog/profiles/{}'.format(profile_number) - resp = requests.get(url) - # Consider any status other than 2xx an error - if not resp.status_code // 100 == 2: - return "Error: Unexpected response {}".format(resp) - profile = resp.json() - self._parse_into_df(profile) - return profile - - def _parse_into_df(self, profiles): - """ - Stores profiles returned by query into dataframe - saves profiles back to self.profiles - returns None - """ - # todo: check that this makes appropriate BGC cols in the DF - # initialize dict - # meas_keys = profiles[0]['bgcMeasKeys'] - # df = pd.DataFrame(columns=meas_keys) - - if not isinstance(profiles, list): - profiles = [profiles] - - # initialise the df (empty or containing previously processed profiles) - if not self.profiles is None: - df = self.profiles - else: - df = pd.DataFrame() - - for profile in profiles: - profileDf = pd.DataFrame(profile['bgcMeas']) - profileDf['cycle_number'] = profile['cycle_number'] - profileDf['profile_id'] = profile['_id'] - profileDf['lat'] = profile['lat'] - profileDf['lon'] = profile['lon'] - profileDf['date'] = profile['date'] - df = pd.concat([df, profileDf], sort=False) - # if self.profiles is None: - # df = pd.concat([df, profileDf], sort=False) - # else: - # df = df.merge(profileDf, on='profile_id') - self.profiles = df - -if __name__ == '__main__': - # no profiles available - # reg_a = BGC_Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) - # 24 profiles available - - reg_a = BGC_Argo([-150, 30, -120, 60], ['2022-06-07', '2022-06-21']) - # reg_a.search_data(['doxy', 'nitrate', 'down_irradiance412'], printURL=True, keep_all=False) - reg_a.search_data(['down_irradiance412'], printURL=True, keep_all=False) - # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) - - # reg_a.download_by_profile('4903026_101') - - # reg_a._validate_parameters(['doxy', - # 'chla', - # 'cdomm',]) - - - # p = reg_a._validate_parameters(['nitrate', 'pres', 'doxy']) - # print(p) + def __init__(self, boundingbox, timeframe): + super().__init__(boundingbox, timeframe) + # self.profiles = None + + def _search_data_BGC_helper(self): + """ + make request with two params, and identify profiles that contain + remaining params + i.e. mandates the intersection of all specified params + """ + pass + + def search_data(self, params, presRange=None, printURL=False, keep_all=True): + + # assert len(params) != 0, 'One or more BGC measurements must be specified.' + + # API request requires exactly 2 measurement params, duplicate single of necessary + if len(params) == 1: + params.append(params[0]) + + # # validate list of user-entered params, sorts into order to be queried + # params = self._validate_parameters(params) + + # builds URL to be submitted + baseURL = "https://argovis.colorado.edu/selection/bgc_data_selection/" + + payload = { + "startDate": self._start.strftime("%Y-%m-%d"), + "endDate": self._end.strftime("%Y-%m-%d"), + "shape": [self._fmt_coordinates()], + "meas_1": params[0], + "meas_2": params[1], + } + + # if presRange: + # payload['presRange'] = presRange + + # # submit request + # resp = requests.get(baseURL, params=payload) + + # if printURL: + # print(resp.url) + + # # Consider any status other than 2xx an error + # if not resp.status_code // 100 == 2: + # msg = "Error: Unexpected response {}".format(resp) + # print(msg) + # return + + # selectionProfiles = resp.json() + + # # check for the existence of profiles from query + # if selectionProfiles == []: + # msg = 'Warning: Query returned no profiles\n' \ + # 'Please try different search parameters' + # print(msg) + # return + + # # deterine which profiles contain all specified params + # prof_ids = self._filter_profiles(selectionProfiles, params) + + # print('{0} valid profiles have been identified'.format(len(prof_ids))) + # iterate and download profiles individually + # for i in prof_ids: + # print("processing profile", i) + # self.download_by_profile(i) + + # self.profiles.reset_index(inplace=True) + + # if not keep_all: + # # drop BGC measurement columns not specified by user + # drop_params = list(set(list(self._valid_BGC_params())[3:]) - set(params)) + # qc_params = [] + # for i in drop_params: + # qc_params.append(i + '_qc') + # drop_params += qc_params + # self.profiles.drop(columns=drop_params, inplace=True, errors='ignore') + + def _valid_BGC_params(self): + """ + This is a list of valid BGC params, stored here to remove redundancy + They are ordered by how commonly they are measured (approx) + """ + params = valid_params = { + "pres": 0, + "temp": 1, + "psal": 2, + "cndx": 3, + "doxy": 4, + "ph_in_situ_total": 5, + "chla": 6, + "cdom": 7, + "nitrate": 8, + "bbp700": 9, + "down_irradiance412": 10, + "down_irradiance442": 11, + "down_irradiance490": 12, + "down_irradiance380": 13, + "downwelling_par": 14, + } + return params + + # def _validate_parameters(self, params): + # ''' + # Asserts that user-specified parameters are valid as per the Argovis documentation here: + # https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_ + + # Returns + # ------- + # the list of params sorted in the order in which they should be queried (least + # commonly available to most commonly available) + # ''' + + # # valid params ordered by how commonly they are measured (approx) + # valid_params = self._valid_BGC_params() + + # # checks that params are valid + # for i in params: + # assert i in valid_params.keys(), \ + # "Parameter '{0}' is not valid. Valid parameters are {1}".format(i, valid_params.keys()) + + # # sorts params into order in which they should be queried + # params = sorted(params, key= lambda i: valid_params[i], reverse=True) + # return params + + def _filter_profiles(self, profiles, params): + """ + from a dictionary of all profiles returned by first API request, remove the + profiles that do not contain ALL BGC measurements specified by user + returns a list of profile ID's that contain all necessary BGC params + """ + # todo: filter out BGC profiles + good_profs = [] + for i in profiles: + bgc_meas = i["bgcMeasKeys"] + check = all(item in bgc_meas for item in params) + if check: + good_profs.append(i["_id"]) + # print(i['_id']) + + # profiles = good_profs + return good_profs + + # def download_by_profile(self, profile_number): + # url = 'https://argovis.colorado.edu/catalog/profiles/{}'.format(profile_number) + # resp = requests.get(url) + # # Consider any status other than 2xx an error + # if not resp.status_code // 100 == 2: + # return "Error: Unexpected response {}".format(resp) + # profile = resp.json() + # self._parse_into_df(profile) + # return profile + + def _parse_into_df(self, profiles): + """ + Stores profiles returned by query into dataframe + saves profiles back to self.profiles + returns None + """ + # todo: check that this makes appropriate BGC cols in the DF + # initialize dict + # meas_keys = profiles[0]['bgcMeasKeys'] + # df = pd.DataFrame(columns=meas_keys) + + if not isinstance(profiles, list): + profiles = [profiles] + + # initialise the df (empty or containing previously processed profiles) + if not self.profiles is None: + df = self.profiles + else: + df = pd.DataFrame() + + for profile in profiles: + profileDf = pd.DataFrame(profile["bgcMeas"]) + profileDf["cycle_number"] = profile["cycle_number"] + profileDf["profile_id"] = profile["_id"] + profileDf["lat"] = profile["lat"] + profileDf["lon"] = profile["lon"] + profileDf["date"] = profile["date"] + df = pd.concat([df, profileDf], sort=False) + # if self.profiles is None: + # df = pd.concat([df, profileDf], sort=False) + # else: + # df = df.merge(profileDf, on='profile_id') + self.profiles = df + + +if __name__ == "__main__": + # no profiles available + # reg_a = BGC_Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) + # 24 profiles available + + reg_a = BGC_Argo([-150, 30, -120, 60], ["2022-06-07", "2022-06-21"]) + # reg_a.search_data(['doxy', 'nitrate', 'down_irradiance412'], printURL=True, keep_all=False) + reg_a.search_data(["down_irradiance412"], printURL=True, keep_all=False) + # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) + + # reg_a.download_by_profile('4903026_101') + + # reg_a._validate_parameters(['doxy', + # 'chla', + # 'cdomm',]) + + # p = reg_a._validate_parameters(['nitrate', 'pres', 'doxy']) + # print(p) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index a06661d33..d53ae4a5b 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -44,12 +44,21 @@ def __init__(self, boundingbox, timeframe): assert self._spatial._ext_type == "bounding_box" self.profiles = None - def search_data(self, presRange=None, printURL=False) -> str: + def search_data(self, params=["all"], presRange=None, printURL=False) -> str: """ - query dataset given the spatio temporal criteria - and other params specific to the dataset + query argo profiles given the spatio temporal criteria + and other params specific to the dataset. + + Returns + ------ + str: message on the success status of the search """ + assert len(params) != 0, "One or more measurements must be specified." + # NOTE: check to see if must have exactly 2 params for download; there's also an "all" option now + params = self._validate_parameters(params) + print(params) + # builds URL to be submitted baseURL = "https://argovis-api.colorado.edu/argo" payload = { @@ -84,11 +93,26 @@ def search_data(self, presRange=None, printURL=False) -> str: print(msg) return msg - # if profiles are found, save them to self as dataframe - msg = "Found profiles - converting to a dataframe" - self._parse_into_df(selectionProfiles) + # deterine which profiles contain all specified params + if "all" in params: + prof_ids = [] + for i in selectionProfiles: + prof_ids.append(i["_id"]) + # print(i['_id']) + else: + prof_ids = self._filter_profiles(selectionProfiles, params) + + self.prof_ids = prof_ids + + msg = "{0} valid profiles have been identified".format(len(prof_ids)) + print(msg) return msg + # # if profiles are found, save them to self as dataframe + # msg = "Found profiles - converting to a dataframe" + # self._parse_into_df(selectionProfiles) + # return msg + def _fmt_coordinates(self) -> str: """ Convert spatial extent into string format needed by argovis @@ -108,8 +132,95 @@ def _fmt_coordinates(self) -> str: x = "[" + x + "]" return x + def _valid_params(self) -> dict: + """ + This is a list of valid Argo params, stored here to remove redundancy + They are ordered by how commonly they are measured (approx) + """ + valid_params = { + "doxy": 0, + "doxy_argoqc": 1, + "pressure": 2, + "pressure_argoqc": 3, + "salinity": 4, + "salinity_argoqc": 5, + "salinity_sfile": 6, + "salinity_sfile_argoqc": 7, + "temperature": 8, + "temperature_argoqc": 9, + "temperature_sfile": 10, + "temperature_sfile_argoqc": 11, + "all": 12, + } + return valid_params + + def _validate_parameters(self, params) -> list: + """ + Asserts that user-specified parameters are valid as per the Argovis documentation here: + https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_ + + Returns + ------- + the list of params sorted in the order in which they should be queried (least + commonly available to most commonly available) + """ + + # valid params ordered by how commonly they are measured (approx) + valid_params = self._valid_params() + + # checks that params are valid + for i in params: + assert ( + i in valid_params.keys() + ), "Parameter '{0}' is not valid. Valid parameters are {1}".format( + i, valid_params.keys() + ) + + # sorts params into order in which they should be queried + params = sorted(params, key=lambda i: valid_params[i], reverse=True) + + if "all" in params: + params = ["all"] + + return params + + def _filter_profiles(self, profiles, params): + """ + from a dictionary of all profiles returned by first API request, remove the + profiles that do not contain ALL measurements specified by user + returns a list of profile ID's that contain all necessary BGC params + """ + # todo: filter out BGC profiles + good_profs = [] + for i in profiles: + avail_meas = i["data_info"][0] + check = all(item in avail_meas for item in params) + if check: + good_profs.append(i["_id"]) + print(i["_id"]) + + return good_profs + + def download_by_profile(self, keep_all=True): + for i in self.prof_ids: + print("processing profile", i) + profile_data = self._download_profile(i) + + self._parse_into_df(profile_data) + self.profiles.reset_index(inplace=True) + + # NEXT STEP: actually construct a properly formatted download (see example) + def _download_profile(self, profile_number): + url = "https://argovis.colorado.edu/catalog/profiles/{}".format(profile_number) + resp = requests.get(url) + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + return "Error: Unexpected response {}".format(resp) + profile = resp.json() + return profile + # todo: add a try/except to make sure the json files are valid i.e. contains all data we're expecting (no params are missing) - def _parse_into_df(self, profiles) -> None: + def _parse_into_df(self, profile_data) -> None: """ Stores profiles returned by query into dataframe saves profiles back to self.profiles @@ -118,21 +229,53 @@ def _parse_into_df(self, profiles) -> None: ------- None """ - # initialize dict - df = pd.DataFrame() - for profile in profiles: - # this line "works", but data is no longer included in the response so the resulting df is useless - profileDf = pd.DataFrame(profile["data_info"]) - # Note: the cycle_number is returned as part of the id: _ - # profileDf["cycle_number"] = profile["cycle_number"] - profileDf["profile_id"] = profile["_id"] - # there's also a geolocation field that provides the geospatial info as shapely points - profileDf["lat"] = profile["geolocation"]["coordinates"][1] - profileDf["lon"] = profile["geolocation"]["coordinates"][0] - profileDf["date"] = profile["timestamp"] - df = pd.concat([df, profileDf], sort=False) + + # NEXT STEPS: + # decide where on the object to store the dataframe (self.profiles? self.argodata?) + + if not self.profiles is None: + df = self.profiles + else: + df = pd.DataFrame() + + # parse the profile data into a dataframe + # this line "works", but data is no longer included in the response so the resulting df is useless + profileDf = pd.DataFrame(profile_data["data_info"]) + # Note: the cycle_number is returned as part of the id: _ + # profileDf["cycle_number"] = profile["cycle_number"] + profileDf["profile_id"] = profile_data["_id"] + # there's also a geolocation field that provides the geospatial info as shapely points + profileDf["lat"] = profile_data["geolocation"]["coordinates"][1] + profileDf["lon"] = profile_data["geolocation"]["coordinates"][0] + profileDf["date"] = profile_data["timestamp"] + + # may need to use the concat or merge if statement in argobgc + df = pd.concat([df, profileDf], sort=False) self.profiles = df + def get_dataframe(self, params, keep_all=True) -> pd.DataFrame: + """ + Downloads the requested data and returns it in a DataFrame + + Returns + ------- + pd.DataFrame: DataFrame of requested data + """ + + self.search_data(params) + self.download_by_profile() + + if not keep_all: + # drop measurement columns not specified by user + drop_params = list(set(list(self._valid_params())[3:]) - set(params)) + qc_params = [] + for i in drop_params: + qc_params.append(i + "_qc") + drop_params += qc_params + self.profiles.drop(columns=drop_params, inplace=True, errors="ignore") + + return self.profiles + # this is just for the purpose of debugging and should be removed later if __name__ == "__main__": @@ -140,5 +283,14 @@ def _parse_into_df(self, profiles) -> None: # reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) # profiles available reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + + # Note: this works; will need to see if it carries through + # Note: run this if you just want valid profile ids (stored as reg_a.prof_ids) + # it's the first step completed in get_dataframe reg_a.search_data(printURL=True) + + reg_a.get_dataframe(params=["pressure", "temperature"]) + # if it works with list of len 2, try with a longer list... + reg_a.get_dataframe() + print(reg_a.profiles[["pres", "temp", "lat", "lon"]].head()) From f9c6a8289bf9b5d02760d28b29a90015f94f2843 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Tue, 6 Jun 2023 16:44:23 -0400 Subject: [PATCH 041/124] basic working argo script --- icepyx/quest/dataset_scripts/argo.py | 42 ++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index d53ae4a5b..b84293718 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -43,6 +43,7 @@ def __init__(self, boundingbox, timeframe): super().__init__(boundingbox, timeframe) assert self._spatial._ext_type == "bounding_box" self.profiles = None + self._apikey = "92259861231b55d32a9c0e4e3a93f4834fc0b6fa" def search_data(self, params=["all"], presRange=None, printURL=False) -> str: """ @@ -70,8 +71,10 @@ def search_data(self, params=["all"], presRange=None, printURL=False) -> str: payload["presRange"] = presRange # submit request - apikey = "92259861231b55d32a9c0e4e3a93f4834fc0b6fa" - resp = requests.get(baseURL, headers={"x-argokey": apikey}, params=payload) + + resp = requests.get( + baseURL, headers={"x-argokey": self._apikey}, params=payload + ) if printURL: print(resp.url) @@ -201,18 +204,31 @@ def _filter_profiles(self, profiles, params): return good_profs - def download_by_profile(self, keep_all=True): + def download_by_profile(self, params, keep_all=True): for i in self.prof_ids: print("processing profile", i) - profile_data = self._download_profile(i) + profile_data = self._download_profile(i, params=params, printURL=True) - self._parse_into_df(profile_data) + self._parse_into_df(profile_data[0]) self.profiles.reset_index(inplace=True) # NEXT STEP: actually construct a properly formatted download (see example) - def _download_profile(self, profile_number): - url = "https://argovis.colorado.edu/catalog/profiles/{}".format(profile_number) - resp = requests.get(url) + def _download_profile(self, profile_number, params=None, printURL=False): + # builds URL to be submitted + baseURL = "https://argovis-api.colorado.edu/argo" + payload = { + "id": profile_number, + "data": params, + } + + # submit request + resp = requests.get( + baseURL, headers={"x-argokey": self._apikey}, params=payload + ) + + if printURL: + print(resp.url) + # Consider any status other than 2xx an error if not resp.status_code // 100 == 2: return "Error: Unexpected response {}".format(resp) @@ -240,7 +256,9 @@ def _parse_into_df(self, profile_data) -> None: # parse the profile data into a dataframe # this line "works", but data is no longer included in the response so the resulting df is useless - profileDf = pd.DataFrame(profile_data["data_info"]) + profileDf = pd.DataFrame( + np.transpose(profile_data["data"]), columns=profile_data["data_info"][0] + ) # Note: the cycle_number is returned as part of the id: _ # profileDf["cycle_number"] = profile["cycle_number"] profileDf["profile_id"] = profile_data["_id"] @@ -263,7 +281,7 @@ def get_dataframe(self, params, keep_all=True) -> pd.DataFrame: """ self.search_data(params) - self.download_by_profile() + self.download_by_profile(params) if not keep_all: # drop measurement columns not specified by user @@ -282,14 +300,14 @@ def get_dataframe(self, params, keep_all=True) -> pd.DataFrame: # no search results # reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) # profiles available - reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) # "2022-04-26"]) # Note: this works; will need to see if it carries through # Note: run this if you just want valid profile ids (stored as reg_a.prof_ids) # it's the first step completed in get_dataframe reg_a.search_data(printURL=True) - reg_a.get_dataframe(params=["pressure", "temperature"]) + reg_a.get_dataframe(params=["pressure", "temperature", "salinity_argoqc"]) # if it works with list of len 2, try with a longer list... reg_a.get_dataframe() From 9c0de9b5198adca961fb3cfbd30b731a0b72e78d Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Tue, 6 Jun 2023 17:11:41 -0400 Subject: [PATCH 042/124] simplify parameter validation (ordered list no longer needed) --- icepyx/quest/dataset_scripts/argo.py | 79 +++++++++++----------------- 1 file changed, 32 insertions(+), 47 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index b84293718..352d5b5cf 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -55,8 +55,6 @@ def search_data(self, params=["all"], presRange=None, printURL=False) -> str: str: message on the success status of the search """ - assert len(params) != 0, "One or more measurements must be specified." - # NOTE: check to see if must have exactly 2 params for download; there's also an "all" option now params = self._validate_parameters(params) print(params) @@ -71,7 +69,6 @@ def search_data(self, params=["all"], presRange=None, printURL=False) -> str: payload["presRange"] = presRange # submit request - resp = requests.get( baseURL, headers={"x-argokey": self._apikey}, params=payload ) @@ -96,12 +93,12 @@ def search_data(self, params=["all"], presRange=None, printURL=False) -> str: print(msg) return msg - # deterine which profiles contain all specified params + # determine which profiles contain all specified params + # Note: this will be done automatically by Argovis during data download if "all" in params: prof_ids = [] for i in selectionProfiles: prof_ids.append(i["_id"]) - # print(i['_id']) else: prof_ids = self._filter_profiles(selectionProfiles, params) @@ -111,11 +108,6 @@ def search_data(self, params=["all"], presRange=None, printURL=False) -> str: print(msg) return msg - # # if profiles are found, save them to self as dataframe - # msg = "Found profiles - converting to a dataframe" - # self._parse_into_df(selectionProfiles) - # return msg - def _fmt_coordinates(self) -> str: """ Convert spatial extent into string format needed by argovis @@ -135,55 +127,48 @@ def _fmt_coordinates(self) -> str: x = "[" + x + "]" return x - def _valid_params(self) -> dict: + # TODO: contact argovis for a list of valid params (swagger api docs are a blank page) + def _valid_params(self) -> list: """ - This is a list of valid Argo params, stored here to remove redundancy - They are ordered by how commonly they are measured (approx) + A list of valid Argo measurement parameters. """ - valid_params = { - "doxy": 0, - "doxy_argoqc": 1, - "pressure": 2, - "pressure_argoqc": 3, - "salinity": 4, - "salinity_argoqc": 5, - "salinity_sfile": 6, - "salinity_sfile_argoqc": 7, - "temperature": 8, - "temperature_argoqc": 9, - "temperature_sfile": 10, - "temperature_sfile_argoqc": 11, - "all": 12, - } + valid_params = [ + "doxy", + "doxy_argoqc", + "pressure", + "pressure_argoqc", + "salinity", + "salinity_argoqc", + "salinity_sfile", + "salinity_sfile_argoqc", + "temperature", + "temperature_argoqc", + "temperature_sfile", + "temperature_sfile_argoqc", + "all", + ] return valid_params def _validate_parameters(self, params) -> list: """ - Asserts that user-specified parameters are valid as per the Argovis documentation here: - https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_ + Checks that the list of user requested parameters are valid. Returns ------- - the list of params sorted in the order in which they should be queried (least - commonly available to most commonly available) + The list of valid parameters """ - # valid params ordered by how commonly they are measured (approx) - valid_params = self._valid_params() - - # checks that params are valid - for i in params: - assert ( - i in valid_params.keys() - ), "Parameter '{0}' is not valid. Valid parameters are {1}".format( - i, valid_params.keys() - ) - - # sorts params into order in which they should be queried - params = sorted(params, key=lambda i: valid_params[i], reverse=True) - if "all" in params: params = ["all"] + else: + valid_params = self._valid_params() + # checks that params are valid + for i in params: + assert ( + i in valid_params + ), "Parameter '{0}' is not valid. Valid parameters are {1}".format( + i, valid_params + ) return params @@ -305,7 +290,7 @@ def get_dataframe(self, params, keep_all=True) -> pd.DataFrame: # Note: this works; will need to see if it carries through # Note: run this if you just want valid profile ids (stored as reg_a.prof_ids) # it's the first step completed in get_dataframe - reg_a.search_data(printURL=True) + reg_a.search_data(presRange=[], printURL=True) reg_a.get_dataframe(params=["pressure", "temperature", "salinity_argoqc"]) # if it works with list of len 2, try with a longer list... From af4d8ce3e5a530552601a8afdbf99c088c5425f8 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Tue, 6 Jun 2023 17:32:47 -0400 Subject: [PATCH 043/124] add option to delete existing data before new download --- icepyx/quest/dataset_scripts/argo.py | 71 +++++++++++++++++----------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 352d5b5cf..8fa95a8dc 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -42,7 +42,7 @@ class Argo(DataSet): def __init__(self, boundingbox, timeframe): super().__init__(boundingbox, timeframe) assert self._spatial._ext_type == "bounding_box" - self.profiles = None + self.argodata = None self._apikey = "92259861231b55d32a9c0e4e3a93f4834fc0b6fa" def search_data(self, params=["all"], presRange=None, printURL=False) -> str: @@ -172,9 +172,10 @@ def _validate_parameters(self, params) -> list: return params + # Note: this function may still be useful for users only looking to search for data, but otherwise this filtering is done during download now def _filter_profiles(self, profiles, params): """ - from a dictionary of all profiles returned by first API request, remove the + from a dictionary of all profiles returned by search API request, remove the profiles that do not contain ALL measurements specified by user returns a list of profile ID's that contain all necessary BGC params """ @@ -195,9 +196,8 @@ def download_by_profile(self, params, keep_all=True): profile_data = self._download_profile(i, params=params, printURL=True) self._parse_into_df(profile_data[0]) - self.profiles.reset_index(inplace=True) + self.argodata.reset_index(inplace=True) - # NEXT STEP: actually construct a properly formatted download (see example) def _download_profile(self, profile_number, params=None, printURL=False): # builds URL to be submitted baseURL = "https://argovis-api.colorado.edu/argo" @@ -223,61 +223,76 @@ def _download_profile(self, profile_number, params=None, printURL=False): # todo: add a try/except to make sure the json files are valid i.e. contains all data we're expecting (no params are missing) def _parse_into_df(self, profile_data) -> None: """ - Stores profiles returned by query into dataframe - saves profiles back to self.profiles + Stores downloaded data from a single profile into dataframe. + Appends data to any existing profile data stored in self.argodata. Returns ------- None """ - # NEXT STEPS: - # decide where on the object to store the dataframe (self.profiles? self.argodata?) - - if not self.profiles is None: - df = self.profiles + if not self.argodata is None: + df = self.argodata else: df = pd.DataFrame() # parse the profile data into a dataframe - # this line "works", but data is no longer included in the response so the resulting df is useless profileDf = pd.DataFrame( np.transpose(profile_data["data"]), columns=profile_data["data_info"][0] ) - # Note: the cycle_number is returned as part of the id: _ - # profileDf["cycle_number"] = profile["cycle_number"] profileDf["profile_id"] = profile_data["_id"] # there's also a geolocation field that provides the geospatial info as shapely points profileDf["lat"] = profile_data["geolocation"]["coordinates"][1] profileDf["lon"] = profile_data["geolocation"]["coordinates"][0] profileDf["date"] = profile_data["timestamp"] - # may need to use the concat or merge if statement in argobgc + # NOTE: may need to use the concat or merge if statement in argobgc df = pd.concat([df, profileDf], sort=False) - self.profiles = df + self.argodata = df - def get_dataframe(self, params, keep_all=True) -> pd.DataFrame: + def get_dataframe(self, params, keep_existing=True) -> pd.DataFrame: """ - Downloads the requested data and returns it in a DataFrame + Downloads the requested data and returns it in a DataFrame. + + Data is also stored in self.argodata. + + Parameters + ---------- + params: list of str + A list of all the measurement parameters requested by the user. + + keep_existing: Boolean, default True + Provides the option to clear any existing downloaded data before downloading more. Returns ------- pd.DataFrame: DataFrame of requested data """ + # TODO: do some basic testing of this block and how the dataframe merging actually behaves + if keep_existing == False: + print( + "Your previously stored data in reg.argodata", + "will be deleted before new data is downloaded.", + ) + self.argodata = None + elif keep_existing == True and hasattr(self, "argodata"): + print( + "The data requested by running this line of code\n", + "will be added to previously downloaded data.", + ) + + # Add qc data for each of the parameters requested + if params == ["all"]: + pass + else: + for p in params: + params.append(p + "_qc") + self.search_data(params) self.download_by_profile(params) - if not keep_all: - # drop measurement columns not specified by user - drop_params = list(set(list(self._valid_params())[3:]) - set(params)) - qc_params = [] - for i in drop_params: - qc_params.append(i + "_qc") - drop_params += qc_params - self.profiles.drop(columns=drop_params, inplace=True, errors="ignore") - - return self.profiles + return self.argodata # this is just for the purpose of debugging and should be removed later From fd18b74b6808fe78e9546edd9a186d370e43504d Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Tue, 6 Jun 2023 17:55:13 -0400 Subject: [PATCH 044/124] continue cleaning up argo.py --- icepyx/quest/dataset_scripts/argo.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 8fa95a8dc..1c81834d8 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -45,11 +45,17 @@ def __init__(self, boundingbox, timeframe): self.argodata = None self._apikey = "92259861231b55d32a9c0e4e3a93f4834fc0b6fa" - def search_data(self, params=["all"], presRange=None, printURL=False) -> str: + def search_data( + self, params=["temperature", "pressure"], presRange=None, printURL=False + ) -> str: """ query argo profiles given the spatio temporal criteria and other params specific to the dataset. + Parameters + --------- + + Returns ------ str: message on the success status of the search @@ -190,7 +196,7 @@ def _filter_profiles(self, profiles, params): return good_profs - def download_by_profile(self, params, keep_all=True): + def download_by_profile(self, params): for i in self.prof_ids: print("processing profile", i) profile_data = self._download_profile(i, params=params, printURL=True) @@ -246,8 +252,13 @@ def _parse_into_df(self, profile_data) -> None: profileDf["lon"] = profile_data["geolocation"]["coordinates"][0] profileDf["date"] = profile_data["timestamp"] - # NOTE: may need to use the concat or merge if statement in argobgc + # TODO: debug this. Currently this just keeps the last profile!! df = pd.concat([df, profileDf], sort=False) + # if self.argodata is None: + # df = pd.concat([df, profileDf], sort=False) + # else: + # df = df.merge(profileDf, on='profile_id') + self.argodata = df def get_dataframe(self, params, keep_existing=True) -> pd.DataFrame: @@ -287,7 +298,10 @@ def get_dataframe(self, params, keep_existing=True) -> pd.DataFrame: pass else: for p in params: - params.append(p + "_qc") + if p.endswith("_argoqc"): + pass + else: + params.append(p + "_argoqc") self.search_data(params) self.download_by_profile(params) From df41a9892baba1afc1e1500c5c4f96127f2921b0 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 7 Jun 2023 11:45:08 -0400 Subject: [PATCH 045/124] fix download_by_profile to properly store all downloaded data --- icepyx/quest/dataset_scripts/argo.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 1c81834d8..1072bde06 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -200,9 +200,8 @@ def download_by_profile(self, params): for i in self.prof_ids: print("processing profile", i) profile_data = self._download_profile(i, params=params, printURL=True) - - self._parse_into_df(profile_data[0]) - self.argodata.reset_index(inplace=True) + self._parse_into_df(profile_data[0]) + self.argodata.reset_index(inplace=True, drop=True) def _download_profile(self, profile_number, params=None, printURL=False): # builds URL to be submitted @@ -252,13 +251,7 @@ def _parse_into_df(self, profile_data) -> None: profileDf["lon"] = profile_data["geolocation"]["coordinates"][0] profileDf["date"] = profile_data["timestamp"] - # TODO: debug this. Currently this just keeps the last profile!! df = pd.concat([df, profileDf], sort=False) - # if self.argodata is None: - # df = pd.concat([df, profileDf], sort=False) - # else: - # df = df.merge(profileDf, on='profile_id') - self.argodata = df def get_dataframe(self, params, keep_existing=True) -> pd.DataFrame: From 27b672bb98fa9c66762a00d99185606e50024e2d Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 7 Jun 2023 13:04:51 -0400 Subject: [PATCH 046/124] remove old get_argo.py script --- get_argo.py | 55 ----------------------------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 get_argo.py diff --git a/get_argo.py b/get_argo.py deleted file mode 100644 index 4e4db6049..000000000 --- a/get_argo.py +++ /dev/null @@ -1,55 +0,0 @@ -import requests -import pandas as pd -import os - - -##### -# Get current directory to save file into - -curDir = os.getcwd() - - -# Get a selected region from Argovis - -def get_selection_profiles(startDate, endDate, shape, presRange=None): - baseURL = 'https://argovis.colorado.edu/selection/profiles/' - startDateQuery = '?startDate=' + startDate - endDateQuery = '&endDate=' + endDate - shapeQuery = '&shape='+shape - if not presRange == None: - pressRangeQuery = '&presRange;=' + presRange - url = baseURL + startDateQuery + endDateQuery + pressRangeQuery + shapeQuery - else: - url = baseURL + startDateQuery + endDateQuery + shapeQuery - resp = requests.get(url) - # Consider any status other than 2xx an error - if not resp.status_code // 100 == 2: - return "Error: Unexpected response {}".format(resp) - selectionProfiles = resp.json() - return selectionProfiles - -## Get platform information -def parse_into_df(profiles): - #initialize dict - meas_keys = profiles[0]['measurements'][0].keys() - df = pd.DataFrame(columns=meas_keys) - for profile in profiles: - profileDf = pd.DataFrame(profile['measurements']) - profileDf['cycle_number'] = profile['cycle_number'] - profileDf['profile_id'] = profile['_id'] - profileDf['lat'] = profile['lat'] - profileDf['lon'] = profile['lon'] - profileDf['date'] = profile['date'] - df = pd.concat([df, profileDf], sort=False) - return df - -# set start date, end date, lat/lon coordinates for the shape of region and pres range - -startDate='2017-9-15' -endDate='2017-10-31' -# shape should be nested array with lon, lat coords. -shape = '[[[-18.6,31.7],[-18.6,37.7],[-5.9,37.7],[-5.9,31.7],[-18.6,31.7]]]' -presRange='[0,30]' -selectionProfiles = get_selection_profiles(startDate, endDate, shape, presRange) -if len(selectionProfiles) > 0: - selectionDf = parse_into_df(selectionProfiles) From 04e392cd9b21060b7bd8c1b445d4796c18412836 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 7 Jun 2023 13:44:27 -0400 Subject: [PATCH 047/124] remove _filter_profiles function in favor of submitting data kwarg in request --- icepyx/quest/dataset_scripts/argo.py | 90 ++++++++++++---------------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 1072bde06..f3b916ea9 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -1,9 +1,9 @@ -from icepyx.quest.dataset_scripts.dataset import DataSet -from icepyx.core.spatial import geodataframe -import requests -import pandas as pd -import os import numpy as np +import pandas as pd +import requests + +from icepyx.core.spatial import geodataframe +from icepyx.quest.dataset_scripts.dataset import DataSet class Argo(DataSet): @@ -31,14 +31,13 @@ class Argo(DataSet): Warning: Query returned no profiles Please try different search parameters - See Also -------- DataSet GenQuery """ - # DevNote: it looks like ArgoVis now accepts polygons, not just bounding boxes + # Note: it looks like ArgoVis now accepts polygons, not just bounding boxes def __init__(self, boundingbox, timeframe): super().__init__(boundingbox, timeframe) assert self._spatial._ext_type == "bounding_box" @@ -49,12 +48,18 @@ def search_data( self, params=["temperature", "pressure"], presRange=None, printURL=False ) -> str: """ - query argo profiles given the spatio temporal criteria + Query argo profiles given the spatio temporal criteria and other params specific to the dataset. Parameters --------- - + params: list of str, default ["temperature", "pressure] + A list of strings, where each string is a requested parameter. + Only metadata for profiles with the requested parameters are returned. + To search for all parameters, use `params=["all"]`. + presRange: str, default None + The pressure range (which correllates with depth) to search for data within. + Input as a "shallow-limit,deep-limit" string. Note the lack of space. Returns ------ @@ -70,6 +75,7 @@ def search_data( "startDate": self._temporal._start.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), "endDate": self._temporal._end.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), "polygon": [self._fmt_coordinates()], + "data": params, } if presRange: payload["presRange"] = presRange @@ -99,15 +105,10 @@ def search_data( print(msg) return msg - # determine which profiles contain all specified params - # Note: this will be done automatically by Argovis during data download - if "all" in params: - prof_ids = [] - for i in selectionProfiles: - prof_ids.append(i["_id"]) - else: - prof_ids = self._filter_profiles(selectionProfiles, params) - + # record the profile ids for the profiles that contain the requested parameters + prof_ids = [] + for i in selectionProfiles: + prof_ids.append(i["_id"]) self.prof_ids = prof_ids msg = "{0} valid profiles have been identified".format(len(prof_ids)) @@ -178,32 +179,18 @@ def _validate_parameters(self, params) -> list: return params - # Note: this function may still be useful for users only looking to search for data, but otherwise this filtering is done during download now - def _filter_profiles(self, profiles, params): - """ - from a dictionary of all profiles returned by search API request, remove the - profiles that do not contain ALL measurements specified by user - returns a list of profile ID's that contain all necessary BGC params - """ - # todo: filter out BGC profiles - good_profs = [] - for i in profiles: - avail_meas = i["data_info"][0] - check = all(item in avail_meas for item in params) - if check: - good_profs.append(i["_id"]) - print(i["_id"]) - - return good_profs - - def download_by_profile(self, params): + def download_by_profile(self, params, presRange=None): for i in self.prof_ids: print("processing profile", i) - profile_data = self._download_profile(i, params=params, printURL=True) + profile_data = self._download_profile( + i, params=params, presRange=presRange, printURL=True + ) self._parse_into_df(profile_data[0]) self.argodata.reset_index(inplace=True, drop=True) - def _download_profile(self, profile_number, params=None, printURL=False): + def _download_profile( + self, profile_number, params=None, presRange=None, printURL=False + ): # builds URL to be submitted baseURL = "https://argovis-api.colorado.edu/argo" payload = { @@ -211,6 +198,9 @@ def _download_profile(self, profile_number, params=None, printURL=False): "data": params, } + if presRange: + payload["presRange"] = presRange + # submit request resp = requests.get( baseURL, headers={"x-argokey": self._apikey}, params=payload @@ -254,7 +244,7 @@ def _parse_into_df(self, profile_data) -> None: df = pd.concat([df, profileDf], sort=False) self.argodata = df - def get_dataframe(self, params, keep_existing=True) -> pd.DataFrame: + def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFrame: """ Downloads the requested data and returns it in a DataFrame. @@ -296,8 +286,8 @@ def get_dataframe(self, params, keep_existing=True) -> pd.DataFrame: else: params.append(p + "_argoqc") - self.search_data(params) - self.download_by_profile(params) + self.search_data(params, presRange=presRange) + self.download_by_profile(params, presRange=presRange) return self.argodata @@ -307,15 +297,13 @@ def get_dataframe(self, params, keep_existing=True) -> pd.DataFrame: # no search results # reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) # profiles available - reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) # "2022-04-26"]) + reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2023-04-13"]) # "2022-04-26"]) - # Note: this works; will need to see if it carries through - # Note: run this if you just want valid profile ids (stored as reg_a.prof_ids) - # it's the first step completed in get_dataframe - reg_a.search_data(presRange=[], printURL=True) + reg_a.search_data(printURL=True) - reg_a.get_dataframe(params=["pressure", "temperature", "salinity_argoqc"]) - # if it works with list of len 2, try with a longer list... - reg_a.get_dataframe() + reg_a.search_data(params=["doxy"]) - print(reg_a.profiles[["pres", "temp", "lat", "lon"]].head()) + reg_a.get_dataframe( + params=["pressure", "temperature", "salinity_argoqc"], presRange="0.2,100" + ) + # if it works with list of len 2, try with a longer list... From 9cc004058b0ae7bd368eca54a63f5187ff2f0db7 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 7 Jun 2023 13:55:45 -0400 Subject: [PATCH 048/124] start filling in docstrings --- icepyx/quest/dataset_scripts/argo.py | 32 +++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index f3b916ea9..b1cb0fa0b 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -48,7 +48,7 @@ def search_data( self, params=["temperature", "pressure"], presRange=None, printURL=False ) -> str: """ - Query argo profiles given the spatio temporal criteria + Query for available argo profiles given the spatio temporal criteria and other params specific to the dataset. Parameters @@ -57,9 +57,12 @@ def search_data( A list of strings, where each string is a requested parameter. Only metadata for profiles with the requested parameters are returned. To search for all parameters, use `params=["all"]`. + For a list of available parameters, see: presRange: str, default None The pressure range (which correllates with depth) to search for data within. Input as a "shallow-limit,deep-limit" string. Note the lack of space. + printURL: boolean, default False + Print the URL of the data request. Useful for debugging and when no data is returned. Returns ------ @@ -117,7 +120,7 @@ def search_data( def _fmt_coordinates(self) -> str: """ - Convert spatial extent into string format needed by argovis + Convert spatial extent into string format needed by argovis API i.e. list of polygon coords [[[lat1,lon1],[lat2,lon2],...]] """ @@ -179,7 +182,29 @@ def _validate_parameters(self, params) -> list: return params - def download_by_profile(self, params, presRange=None): + def download_by_profile(self, params, presRange=None) -> None: + """ + For a list of profiles IDs (stored under .prof_ids), download the data requested for each one. + + Parameters + ---------- + params: list of str, default ["temperature", "pressure] + A list of strings, where each string is a requested parameter. + Only metadata for profiles with the requested parameters are returned. + To search for all parameters, use `params=["all"]`. + For a list of available parameters, see: + presRange: str, default None + The pressure range (which correllates with depth) to search for data within. + Input as a "shallow-limit,deep-limit" string. Note the lack of space. + + Returns + ------- + None; outputs are stored in the .argodata property. + """ + # TODO: Need additional checks here? + if not hasattr(self, "prof_ids"): + self.search_data(params, presRange=presRange) + for i in self.prof_ids: print("processing profile", i) profile_data = self._download_profile( @@ -244,6 +269,7 @@ def _parse_into_df(self, profile_data) -> None: df = pd.concat([df, profileDf], sort=False) self.argodata = df + # next steps: reconcile download by profile and get_dataframe. They need to have clearer division of labor def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFrame: """ Downloads the requested data and returns it in a DataFrame. From d15483b49a7c6af980eafbdd00b72c13c082f84c Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Thu, 8 Jun 2023 16:02:12 -0400 Subject: [PATCH 049/124] clean up nearly duplicate functions --- icepyx/quest/dataset_scripts/argo.py | 67 +++++++++++----------------- 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index b1cb0fa0b..859affcd7 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -182,39 +182,12 @@ def _validate_parameters(self, params) -> list: return params - def download_by_profile(self, params, presRange=None) -> None: - """ - For a list of profiles IDs (stored under .prof_ids), download the data requested for each one. - - Parameters - ---------- - params: list of str, default ["temperature", "pressure] - A list of strings, where each string is a requested parameter. - Only metadata for profiles with the requested parameters are returned. - To search for all parameters, use `params=["all"]`. - For a list of available parameters, see: - presRange: str, default None - The pressure range (which correllates with depth) to search for data within. - Input as a "shallow-limit,deep-limit" string. Note the lack of space. - - Returns - ------- - None; outputs are stored in the .argodata property. - """ - # TODO: Need additional checks here? - if not hasattr(self, "prof_ids"): - self.search_data(params, presRange=presRange) - - for i in self.prof_ids: - print("processing profile", i) - profile_data = self._download_profile( - i, params=params, presRange=presRange, printURL=True - ) - self._parse_into_df(profile_data[0]) - self.argodata.reset_index(inplace=True, drop=True) - def _download_profile( - self, profile_number, params=None, presRange=None, printURL=False + self, + profile_number, + params=None, + presRange=None, + printURL=False, ): # builds URL to be submitted baseURL = "https://argovis-api.colorado.edu/argo" @@ -269,18 +242,22 @@ def _parse_into_df(self, profile_data) -> None: df = pd.concat([df, profileDf], sort=False) self.argodata = df - # next steps: reconcile download by profile and get_dataframe. They need to have clearer division of labor def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFrame: """ - Downloads the requested data and returns it in a DataFrame. + Downloads the requested data for a list of profile IDs (stored under .prof_ids) and returns it in a DataFrame. Data is also stored in self.argodata. Parameters ---------- - params: list of str - A list of all the measurement parameters requested by the user. - + params: list of str, default ["temperature", "pressure] + A list of strings, where each string is a requested parameter. + Only metadata for profiles with the requested parameters are returned. + To search for all parameters, use `params=["all"]`. + For a list of available parameters, see: + presRange: str, default None + The pressure range (which correllates with depth) to search for data within. + Input as a "shallow-limit,deep-limit" string. Note the lack of space. keep_existing: Boolean, default True Provides the option to clear any existing downloaded data before downloading more. @@ -312,8 +289,17 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr else: params.append(p + "_argoqc") - self.search_data(params, presRange=presRange) - self.download_by_profile(params, presRange=presRange) + # TODO: Need additional checks here? + if not hasattr(self, "prof_ids"): + self.search_data(params, presRange=presRange) + + for i in self.prof_ids: + print("processing profile", i) + profile_data = self._download_profile( + i, params=params, presRange=presRange, printURL=True + ) + self._parse_into_df(profile_data[0]) + self.argodata.reset_index(inplace=True, drop=True) return self.argodata @@ -323,7 +309,7 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr # no search results # reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) # profiles available - reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2023-04-13"]) # "2022-04-26"]) + reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) # "2022-04-26"]) reg_a.search_data(printURL=True) @@ -332,4 +318,3 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr reg_a.get_dataframe( params=["pressure", "temperature", "salinity_argoqc"], presRange="0.2,100" ) - # if it works with list of len 2, try with a longer list... From d877e8b1e509d507cd4ae385c30b4c3a04fa79ce Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Thu, 8 Jun 2023 16:11:57 -0400 Subject: [PATCH 050/124] add more docstrings --- icepyx/quest/dataset_scripts/argo.py | 35 ++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 859affcd7..d9badbe32 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -188,7 +188,30 @@ def _download_profile( params=None, presRange=None, printURL=False, - ): + ) -> dict: + """ + Download available argo data for a particular profile_ID. + + Parameters + --------- + profile_number: str + String containing the argo profile ID of the data being downloaded. + params: list of str, default ["temperature", "pressure] + A list of strings, where each string is a requested parameter. + Only data for the requested parameters are returned. + To download all parameters, use `params=["all"]`. + For a list of available parameters, see: + presRange: str, default None + The pressure range (which correllates with depth) to download data within. + Input as a "shallow-limit,deep-limit" string. Note the lack of space. + printURL: boolean, default False + Print the URL of the data request. Useful for debugging and when no data is returned. + + Returns + ------ + dict: json formatted dictionary of the profile data + """ + # builds URL to be submitted baseURL = "https://argovis-api.colorado.edu/argo" payload = { @@ -217,7 +240,15 @@ def _download_profile( def _parse_into_df(self, profile_data) -> None: """ Stores downloaded data from a single profile into dataframe. - Appends data to any existing profile data stored in self.argodata. + Appends data to any existing profile data stored in the `argodata` property. + + Parameters + ---------- + profile_data: dict + The downloaded profile data. + The data is contained in the requests response and converted into a json formatted dictionary + by `_download_profile` before being passed into this function. + Returns ------- From f9b6d8103686f216a1ab1ad28f77fa533dfa39f4 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 12 Jun 2023 11:39:59 -0400 Subject: [PATCH 051/124] get a few minimal argo tests working --- icepyx/quest/dataset_scripts/argo.py | 42 +++++++------- icepyx/tests/test_quest_argo.py | 83 ++++++++++++++++------------ 2 files changed, 70 insertions(+), 55 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index d9badbe32..c4a4a5f02 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -45,7 +45,7 @@ def __init__(self, boundingbox, timeframe): self._apikey = "92259861231b55d32a9c0e4e3a93f4834fc0b6fa" def search_data( - self, params=["temperature", "pressure"], presRange=None, printURL=False + self, params=["temperature"], presRange=None, printURL=False ) -> str: """ Query for available argo profiles given the spatio temporal criteria @@ -91,22 +91,23 @@ def search_data( if printURL: print(resp.url) + selectionProfiles = resp.json() + # Consider any status other than 2xx an error if not resp.status_code // 100 == 2: - msg = "Error: Unexpected response {}".format(resp) - print(msg) - return msg - - selectionProfiles = resp.json() + # check for the existence of profiles from query + if selectionProfiles == []: + msg = ( + "Warning: Query returned no profiles\n" + "Please try different search parameters" + ) + print(msg) + return msg - # check for the existence of profiles from query - if selectionProfiles == []: - msg = ( - "Warning: Query returned no profiles\n" - "Please try different search parameters" - ) - print(msg) - return msg + else: + msg = "Error: Unexpected response {}".format(resp) + print(msg) + return msg # record the profile ids for the profiles that contain the requested parameters prof_ids = [] @@ -338,14 +339,15 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr # this is just for the purpose of debugging and should be removed later if __name__ == "__main__": # no search results - # reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) + reg_a = Argo([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) # profiles available - reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) # "2022-04-26"]) + # reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) # "2022-04-26"]) reg_a.search_data(printURL=True) - reg_a.search_data(params=["doxy"]) + # reg_a.search_data(params=["doxy"]) + + reg_a.get_dataframe(params=["salinity"]) # , presRange="0.2,100" + # ) - reg_a.get_dataframe( - params=["pressure", "temperature", "salinity_argoqc"], presRange="0.2,100" - ) + print(reg_a) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index 0f47fee61..f2d8a56b9 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -1,31 +1,16 @@ -import icepyx as ipx -import json import pytest -import warnings +import re + from icepyx.quest.dataset_scripts.argo import Argo def test_available_profiles(): reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) obs_msg = reg_a.search_data() - obs_cols = reg_a.profiles.columns - exp_msg = "Found profiles - converting to a dataframe" - exp_cols = [ - "pres", - "temp", - "cycle_number", - "profile_id", - "lat", - "lon", - "date", - "psal", - ] + exp_msg = "19 valid profiles have been identified" assert obs_msg == exp_msg - assert set(exp_cols) == set(obs_cols) - - # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) def test_no_available_profiles(): @@ -43,27 +28,55 @@ def test_fmt_coordinates(): reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) obs = reg_a._fmt_coordinates() - exp = "[[[-143.0,30.0],[-143.0,37.0],[-154.0,37.0],[-154.0,30.0],[-143.0,30.0]]]" + exp = "[[-143.0,30.0],[-143.0,37.0],[-154.0,37.0],[-154.0,30.0],[-143.0,30.0]]" assert obs == exp -def test_parse_into_df(): - reg_a = Argo([-154, 54, -151, 56], ["2023-01-20", "2023-01-29"]) - reg_a.search_data() - obs_df = reg_a.profiles +def test_invalid_param(): + reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + + invalid_params = ["temp", "temperature_files"] + + ermsg = re.escape( + "Parameter '{0}' is not valid. Valid parameters are {1}".format( + "temp", reg_a._valid_params() + ) + ) + + with pytest.raises(AssertionError, match=ermsg): + reg_a._validate_parameters(invalid_params) + + +def test_download_parse_into_df(): + reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) + # reg_a.search_data() + reg_a.get_dataframe(params=["salinity"]) # note: pressure is returned by default + + obs_cols = reg_a.argodata.columns + + exp_cols = [ + "salinity", + "salinity_argoqc", + "pressure", + "profile_id", + "lat", + "lon", + "date", + ] + + assert set(exp_cols) == set(obs_cols) + + assert len(reg_a.argodata) == 1943 + - exp = 0 - with open("./icepyx/tests/argovis_test_data2.json") as file: - data = json.load(file) - for profile in data: - exp = exp + len(profile["measurements"]) +""" +def test_presRange_input_param(): + reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) + reg_a.get_dataframe(params=["salinity"], presRange="0.2,100") - assert exp == len(obs_df) +""" - # goal: check number of rows in df matches rows in json - # approach: create json files with profiles and store them in test suite - # then use those for the comparison - # update: for some reason the file downloaded from argovis and the one created here had different lengths - # by downloading the json from the url of the search here, they matched - # next steps: testing argo bgc? +# goal: check number of rows in df matches rows in json +# approach: create json files with profiles and store them in test suite +# then use those for the comparison From 8fcab13420588964aeadcff896df12b2491c19b9 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Tue, 20 Jun 2023 17:07:14 -0400 Subject: [PATCH 052/124] add bgc argo params. begin adding merge for second download runs --- icepyx/quest/dataset_scripts/argo.py | 108 +++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 15 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index c4a4a5f02..b8d202155 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -138,14 +138,15 @@ def _fmt_coordinates(self) -> str: x = "[" + x + "]" return x - # TODO: contact argovis for a list of valid params (swagger api docs are a blank page) def _valid_params(self) -> list: """ - A list of valid Argo measurement parameters. + A list of valid Argo measurement parameters (including BGC). + + To get a list of valid parameters, comment out the validation line in `search_data` herein, + submit a search with an invalid parameter, and get the list from the response. """ valid_params = [ - "doxy", - "doxy_argoqc", + # all argo "pressure", "pressure_argoqc", "salinity", @@ -156,6 +157,66 @@ def _valid_params(self) -> list: "temperature_argoqc", "temperature_sfile", "temperature_sfile_argoqc", + # BGC params + "bbp470", + "bbp470_argoqc", + "bbp532", + "bbp532_argoqc", + "bbp700", + "bbp700_argoqc", + "bbp700_2", + "bbp700_2_argoqc", + "bisulfide", + "bisulfide_argoqc", + "cdom", + "cdom_argoqc", + "chla", + "chla_argoqc", + "cndc", + "cndc_argoqc", + "cndx", + "cndx_argoqc", + "cp660", + "cp660_argoqc", + "down_irradiance380", + "down_irradiance380_argoqc", + "down_irradiance412", + "down_irradiance412_argoqc", + "down_irradiance442", + "down_irradiance442_argoqc", + "down_irradiance443", + "down_irradiance443_argoqc", + "down_irradiance490", + "down_irradiance490_argoqc", + "down_irradiance555", + "down_irradiance555_argoqc", + "down_irradiance670", + "down_irradiance670_argoqc", + "downwelling_par", + "downwelling_par_argoqc", + "doxy", + "doxy_argoqc", + "doxy2", + "doxy2_argoqc", + "doxy3", + "doxy3_argoqc", + "molar_doxy", + "molar_doxy_argoqc", + "nitrate", + "nitrate_argoqc", + "ph_in_situ_total", + "ph_in_situ_total_argoqc", + "turbidity", + "turbidity_argoqc", + "up_radiance412", + "up_radiance412_argoqc", + "up_radiance443", + "up_radiance443_argoqc", + "up_radiance490", + "up_radiance490_argoqc", + "up_radiance555", + "up_radiance555_argoqc", + # all params "all", ] return valid_params @@ -259,7 +320,7 @@ def _parse_into_df(self, profile_data) -> None: if not self.argodata is None: df = self.argodata else: - df = pd.DataFrame() + df = pd.DataFrame(columns=["profile_id"]) # parse the profile data into a dataframe profileDf = pd.DataFrame( @@ -271,7 +332,13 @@ def _parse_into_df(self, profile_data) -> None: profileDf["lon"] = profile_data["geolocation"]["coordinates"][0] profileDf["date"] = profile_data["timestamp"] - df = pd.concat([df, profileDf], sort=False) + if profile_data["_id"] in df["profile_id"].unique(): + print("merging") + df = df.merge(profileDf, how="outer") + else: + print("concatting") + df = pd.concat([df, profileDf], sort=False) + self.argodata = df def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFrame: @@ -286,7 +353,7 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr A list of strings, where each string is a requested parameter. Only metadata for profiles with the requested parameters are returned. To search for all parameters, use `params=["all"]`. - For a list of available parameters, see: + For a list of available parameters, see: `reg._valid_params` presRange: str, default None The pressure range (which correllates with depth) to search for data within. Input as a "shallow-limit,deep-limit" string. Note the lack of space. @@ -316,14 +383,13 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr pass else: for p in params: - if p.endswith("_argoqc"): + if p.endswith("_argoqc") or (p + "_argoqc" in params): pass else: params.append(p + "_argoqc") - # TODO: Need additional checks here? - if not hasattr(self, "prof_ids"): - self.search_data(params, presRange=presRange) + # intentionally resubmit search to reset prof_ids, in case the user requested different parameters + self.search_data(params, presRange=presRange) for i in self.prof_ids: print("processing profile", i) @@ -339,15 +405,27 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr # this is just for the purpose of debugging and should be removed later if __name__ == "__main__": # no search results - reg_a = Argo([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) + # reg_a = Argo([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) # profiles available # reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) # "2022-04-26"]) - reg_a.search_data(printURL=True) + # bgc profiles available + reg_a = Argo([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) + + param_list = ["down_irradiance412"] + bad_param = ["up_irradiance412"] + # param_list = ["doxy"] + + # reg_a.search_data(params=bad_param, printURL=True) - # reg_a.search_data(params=["doxy"]) + reg_a.get_dataframe(params=param_list) - reg_a.get_dataframe(params=["salinity"]) # , presRange="0.2,100" + # next steps: + # the merging results in changing column types from float64 to object (to introduce nans), + # which then means a future merge can't be completed on those columns + # so will need to figure out how to handle this (and can't just convert pressure, so will need to + # handle it for an unknown list of params) + reg_a.get_dataframe(params=["doxy"], keep_existing=True) # , presRange="0.2,100" # ) print(reg_a) From aad50532306faaf60e73295c362272ed9b01a5f7 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 26 Jun 2023 11:44:18 -0400 Subject: [PATCH 053/124] some changes --- icepyx/quest/dataset_scripts/argo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 9fb18760e..ecf657877 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -41,6 +41,7 @@ class Argo(DataSet): def __init__(self, boundingbox, timeframe): super().__init__(boundingbox, timeframe) assert self._spatial._ext_type == "bounding_box" + self.argovis_api_key = '' self.profiles = None def search_data(self, presRange=None, printURL=False) -> str: @@ -50,12 +51,14 @@ def search_data(self, presRange=None, printURL=False) -> str: """ # builds URL to be submitted - baseURL = "https://argovis.colorado.edu/selection/profiles/" + baseURL = "https://argovis-api.colorado.edu/" payload = { "startDate": self._start.strftime("%Y-%m-%d"), "endDate": self._end.strftime("%Y-%m-%d"), "shape": [self._fmt_coordinates()], } + + # dl = requests.get(apiroot + route, params=options, headers={'x-argokey': apikey}) if presRange: payload["presRange"] = presRange From 630415a3aec284cec4e8f7a4639ee0cdd0119cc8 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Fri, 7 Jul 2023 12:56:37 -0400 Subject: [PATCH 054/124] WIP test commit to see if can push to GH --- icepyx/quest/dataset_scripts/argo.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index b8d202155..c8a4eb735 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -332,9 +332,12 @@ def _parse_into_df(self, profile_data) -> None: profileDf["lon"] = profile_data["geolocation"]["coordinates"][0] profileDf["date"] = profile_data["timestamp"] + profileDf.replace("None", np.nan, inplace=True, regex=True) + if profile_data["_id"] in df["profile_id"].unique(): print("merging") df = df.merge(profileDf, how="outer") + else: print("concatting") df = pd.concat([df, profileDf], sort=False) @@ -425,6 +428,8 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr # which then means a future merge can't be completed on those columns # so will need to figure out how to handle this (and can't just convert pressure, so will need to # handle it for an unknown list of params) + # try fillna with a fill value and downcasting (use inplace flag)... in reality float64 should be able to hold NaNs! + # so maybe it's something else causing the problem? reg_a.get_dataframe(params=["doxy"], keep_existing=True) # , presRange="0.2,100" # ) From fe075405848cb123ca967ce6bba8e3b06ad6b931 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 12 Jul 2023 15:42:39 -0400 Subject: [PATCH 055/124] WIP handling argo merge issue --- .../example_notebooks/IS2_data_variables.ipynb | 4 ++-- icepyx/quest/__init__.py | 1 + icepyx/quest/dataset_scripts/argo.py | 13 ++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/source/example_notebooks/IS2_data_variables.ipynb b/doc/source/example_notebooks/IS2_data_variables.ipynb index 568027550..f821e8f3d 100644 --- a/doc/source/example_notebooks/IS2_data_variables.ipynb +++ b/doc/source/example_notebooks/IS2_data_variables.ipynb @@ -327,7 +327,7 @@ "metadata": {}, "outputs": [], "source": [ - "var_dict = region_a.order_vars.append(beam_list=['gt1l', 'gt2l'], var_list=['latitude'])\n", + "region_a.order_vars.append(beam_list=['gt1l', 'gt2l'], var_list=['latitude'])\n", "pprint(region_a.order_vars.wanted)" ] }, @@ -773,7 +773,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.10.10" } }, "nbformat": 4, diff --git a/icepyx/quest/__init__.py b/icepyx/quest/__init__.py index e69de29bb..8b2d18458 100644 --- a/icepyx/quest/__init__.py +++ b/icepyx/quest/__init__.py @@ -0,0 +1 @@ +from .dataset_scripts.argo import Argo diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index c8a4eb735..6a76bdb0c 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -338,6 +338,12 @@ def _parse_into_df(self, profile_data) -> None: print("merging") df = df.merge(profileDf, how="outer") + # pandas isn't excelling because of trying to concat or merge for each profile added. + # if the columns have already been concatted for another profile, we'd need to update to replace nans, not merge + # if the columns haven't been concatted, we'd need to merge. + # options: check for columns and have merge and update and concat pathways OR constructe a df for each request and just merge them after the fact (which might make more sense conceptually and be easier to debug) + + # plan: update this fn to return a df. Do the df merging/concat/updating elsewhere else: print("concatting") df = pd.concat([df, profileDf], sort=False) @@ -423,13 +429,6 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr reg_a.get_dataframe(params=param_list) - # next steps: - # the merging results in changing column types from float64 to object (to introduce nans), - # which then means a future merge can't be completed on those columns - # so will need to figure out how to handle this (and can't just convert pressure, so will need to - # handle it for an unknown list of params) - # try fillna with a fill value and downcasting (use inplace flag)... in reality float64 should be able to hold NaNs! - # so maybe it's something else causing the problem? reg_a.get_dataframe(params=["doxy"], keep_existing=True) # , presRange="0.2,100" # ) From c246543963532e959d18e02666d334e9d58138b1 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Thu, 20 Jul 2023 09:46:37 -0400 Subject: [PATCH 056/124] update profile to df to return df and move merging to get_dataframe --- icepyx/quest/dataset_scripts/argo.py | 55 +++++++++++++++------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 6a76bdb0c..a2fe1a5f6 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -299,9 +299,9 @@ def _download_profile( return profile # todo: add a try/except to make sure the json files are valid i.e. contains all data we're expecting (no params are missing) - def _parse_into_df(self, profile_data) -> None: + def _parse_into_df(self, profile_data) -> pd.DataFrame: """ - Stores downloaded data from a single profile into dataframe. + Parses downloaded data from a single profile into dataframe. Appends data to any existing profile data stored in the `argodata` property. Parameters @@ -314,15 +314,9 @@ def _parse_into_df(self, profile_data) -> None: Returns ------- - None + pandas DataFrame of the profile data """ - if not self.argodata is None: - df = self.argodata - else: - df = pd.DataFrame(columns=["profile_id"]) - - # parse the profile data into a dataframe profileDf = pd.DataFrame( np.transpose(profile_data["data"]), columns=profile_data["data_info"][0] ) @@ -334,21 +328,7 @@ def _parse_into_df(self, profile_data) -> None: profileDf.replace("None", np.nan, inplace=True, regex=True) - if profile_data["_id"] in df["profile_id"].unique(): - print("merging") - df = df.merge(profileDf, how="outer") - - # pandas isn't excelling because of trying to concat or merge for each profile added. - # if the columns have already been concatted for another profile, we'd need to update to replace nans, not merge - # if the columns haven't been concatted, we'd need to merge. - # options: check for columns and have merge and update and concat pathways OR constructe a df for each request and just merge them after the fact (which might make more sense conceptually and be easier to debug) - - # plan: update this fn to return a df. Do the df merging/concat/updating elsewhere - else: - print("concatting") - df = pd.concat([df, profileDf], sort=False) - - self.argodata = df + return profileDf def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFrame: """ @@ -400,17 +380,40 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr # intentionally resubmit search to reset prof_ids, in case the user requested different parameters self.search_data(params, presRange=presRange) + # create a list for each profile's dataframe + if not self.argodata is None: + profile_dfs = [self.argodata] + else: + profile_dfs = [] + for i in self.prof_ids: print("processing profile", i) profile_data = self._download_profile( i, params=params, presRange=presRange, printURL=True ) - self._parse_into_df(profile_data[0]) - self.argodata.reset_index(inplace=True, drop=True) + profile_dfs.append(self._parse_into_df(profile_data[0])) + + self.argodata = pd.merge(profile_dfs, how="outer") + self.argodata.reset_index(inplace=True, drop=True) return self.argodata +""" + # pandas isn't excelling because of trying to concat or merge for each profile added. + # if the columns have already been concatted for another profile, we'd need to update to replace nans, not merge + # if the columns haven't been concatted, we'd need to merge. + # options: check for columns and have merge and update and concat pathways OR constructe a df for each request and just merge them after the fact + # (which might make more sense conceptually and be easier to debug) + if profile_data["_id"] in df["profile_id"].unique(): + print("merging") + df = df.merge(profileDf, how="outer") + else: + print("concatting") + df = pd.concat([df, profileDf], sort=False) +""" + + # this is just for the purpose of debugging and should be removed later if __name__ == "__main__": # no search results From 1fd069cdcd90b45ae81708160237a89e0110dfdd Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 31 Jul 2023 11:30:10 -0400 Subject: [PATCH 057/124] merge profiles with existing df --- icepyx/quest/dataset_scripts/argo.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index a2fe1a5f6..0f3c90a2c 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -380,20 +380,23 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr # intentionally resubmit search to reset prof_ids, in case the user requested different parameters self.search_data(params, presRange=presRange) - # create a list for each profile's dataframe - if not self.argodata is None: - profile_dfs = [self.argodata] - else: - profile_dfs = [] - + # create a dataframe for each profile and merge it with the rest of the profiles from this set of parameters being downloaded + merged_df = pd.DataFrame(columns=["profile_id"]) for i in self.prof_ids: print("processing profile", i) profile_data = self._download_profile( i, params=params, presRange=presRange, printURL=True ) - profile_dfs.append(self._parse_into_df(profile_data[0])) + profile_df = self._parse_into_df(profile_data[0]) + merged_df = pd.concat([merged_df, profile_df], sort=False) + + # now that we have a df from this round of downloads, we can add it to any existing dataframe + # note that if a given column has previously been added, update needs to be used to replace nans (merge will not replace the nan values) + if not self.argodata is None: + self.argodata = self.argodata.merge(merged_df, how="outer") + else: + self.argodata = merged_df - self.argodata = pd.merge(profile_dfs, how="outer") self.argodata.reset_index(inplace=True, drop=True) return self.argodata From 363dad21994dd0852d5077093000949f2b6656b6 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 31 Jul 2023 11:44:34 -0400 Subject: [PATCH 058/124] clean up docstrings and code --- icepyx/quest/dataset_scripts/argo.py | 50 +++++++++++----------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 0f3c90a2c..d0e7cf978 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -9,8 +9,7 @@ class Argo(DataSet): """ Initialises an Argo Dataset object - Used to query physical Argo profiles - -> biogeochemical Argo (BGC) not included + Used to query physical and BGC Argo profiles Examples -------- @@ -53,11 +52,11 @@ def search_data( Parameters --------- - params: list of str, default ["temperature", "pressure] + params: list of str, default ["temperature"] A list of strings, where each string is a requested parameter. Only metadata for profiles with the requested parameters are returned. - To search for all parameters, use `params=["all"]`. - For a list of available parameters, see: + To search for all parameters, use `params=["all"]`; + be careful using all for floats with BGC data, as this may be result in a large download. presRange: str, default None The pressure range (which correllates with depth) to search for data within. Input as a "shallow-limit,deep-limit" string. Note the lack of space. @@ -258,11 +257,10 @@ def _download_profile( --------- profile_number: str String containing the argo profile ID of the data being downloaded. - params: list of str, default ["temperature", "pressure] + params: list of str, default None A list of strings, where each string is a requested parameter. Only data for the requested parameters are returned. To download all parameters, use `params=["all"]`. - For a list of available parameters, see: presRange: str, default None The pressure range (which correllates with depth) to download data within. Input as a "shallow-limit,deep-limit" string. Note the lack of space. @@ -298,7 +296,6 @@ def _download_profile( profile = resp.json() return profile - # todo: add a try/except to make sure the json files are valid i.e. contains all data we're expecting (no params are missing) def _parse_into_df(self, profile_data) -> pd.DataFrame: """ Parses downloaded data from a single profile into dataframe. @@ -311,7 +308,6 @@ def _parse_into_df(self, profile_data) -> pd.DataFrame: The data is contained in the requests response and converted into a json formatted dictionary by `_download_profile` before being passed into this function. - Returns ------- pandas DataFrame of the profile data @@ -320,11 +316,20 @@ def _parse_into_df(self, profile_data) -> pd.DataFrame: profileDf = pd.DataFrame( np.transpose(profile_data["data"]), columns=profile_data["data_info"][0] ) - profileDf["profile_id"] = profile_data["_id"] - # there's also a geolocation field that provides the geospatial info as shapely points - profileDf["lat"] = profile_data["geolocation"]["coordinates"][1] - profileDf["lon"] = profile_data["geolocation"]["coordinates"][0] - profileDf["date"] = profile_data["timestamp"] + + # this block tries to catch changes to the ArgoVis API that will break the dataframe creation + try: + profileDf["profile_id"] = profile_data["_id"] + # there's also a geolocation field that provides the geospatial info as shapely points + profileDf["lat"] = profile_data["geolocation"]["coordinates"][1] + profileDf["lon"] = profile_data["geolocation"]["coordinates"][0] + profileDf["date"] = profile_data["timestamp"] + except KeyError as err: + msg = "We cannot automatically parse your profile into a dataframe due to {0}".format( + err + ) + print(msg) + return msg profileDf.replace("None", np.nan, inplace=True, regex=True) @@ -402,22 +407,7 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr return self.argodata -""" - # pandas isn't excelling because of trying to concat or merge for each profile added. - # if the columns have already been concatted for another profile, we'd need to update to replace nans, not merge - # if the columns haven't been concatted, we'd need to merge. - # options: check for columns and have merge and update and concat pathways OR constructe a df for each request and just merge them after the fact - # (which might make more sense conceptually and be easier to debug) - if profile_data["_id"] in df["profile_id"].unique(): - print("merging") - df = df.merge(profileDf, how="outer") - else: - print("concatting") - df = pd.concat([df, profileDf], sort=False) -""" - - -# this is just for the purpose of debugging and should be removed later +# this is just for the purpose of debugging and should be removed later (after being turned into tests) if __name__ == "__main__": # no search results # reg_a = Argo([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) From 63d3b3b15d642e7142b765525fab0de5d8312603 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 31 Jul 2023 11:46:48 -0400 Subject: [PATCH 059/124] add test_argo.py --- icepyx/tests/test_argo.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 icepyx/tests/test_argo.py diff --git a/icepyx/tests/test_argo.py b/icepyx/tests/test_argo.py new file mode 100644 index 000000000..2395f06c8 --- /dev/null +++ b/icepyx/tests/test_argo.py @@ -0,0 +1,18 @@ +# import icepyx as ipx +import pytest +import warnings +from icepyx.quest import Argo + +def test_validate_params(): + reg_a = Argo([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) + param_list = ["down_irradiance412"] + bad_param = ["up_irradiance412"] + # param_list = ["doxy"] + + # reg_a.search_data(params=bad_param, printURL=True) + + df = reg_a.get_dataframe(params=param_list) + assert 1==1 + +def test_abc(): + assert 1 == 1 \ No newline at end of file From 4602cdb83529c730063240728fa9ae82711318db Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 31 Jul 2023 13:39:22 -0400 Subject: [PATCH 060/124] add prelim test case for adding to Argo df --- icepyx/tests/test_argo.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/icepyx/tests/test_argo.py b/icepyx/tests/test_argo.py index 2395f06c8..182b4727f 100644 --- a/icepyx/tests/test_argo.py +++ b/icepyx/tests/test_argo.py @@ -3,16 +3,25 @@ import warnings from icepyx.quest import Argo -def test_validate_params(): +def test_merge_df(): reg_a = Argo([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) - param_list = ["down_irradiance412"] + param_list = ["salinity", "temperature","down_irradiance412"] bad_param = ["up_irradiance412"] # param_list = ["doxy"] # reg_a.search_data(params=bad_param, printURL=True) df = reg_a.get_dataframe(params=param_list) - assert 1==1 + print(df.columns) + assert "down_irradiance412" in df.columns + assert "down_irradiance412_argoqc" in df.columns + + df = reg_a.get_dataframe(["doxy"], keep_existing=True) + assert "doxy" in df.columns + assert "doxy_argoqc" in df.columns + assert "down_irradiance412" in df.columns + assert "down_irradiance412_argoqc" in df.columns + def test_abc(): assert 1 == 1 \ No newline at end of file From 2cdf07e964d9561029df5308de1cd395c1f6c2dd Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 14 Aug 2023 11:52:30 -0400 Subject: [PATCH 061/124] remove sandbox files --- argo_BGC_class_sandbox | 154 ----------------- get_BGC_argo.py | 96 ----------- icepyx/quest/dataset_scripts/BGCargo.py | 216 ------------------------ 3 files changed, 466 deletions(-) delete mode 100644 argo_BGC_class_sandbox delete mode 100644 get_BGC_argo.py delete mode 100644 icepyx/quest/dataset_scripts/BGCargo.py diff --git a/argo_BGC_class_sandbox b/argo_BGC_class_sandbox deleted file mode 100644 index a0dda63f5..000000000 --- a/argo_BGC_class_sandbox +++ /dev/null @@ -1,154 +0,0 @@ -import requests # dependency for icepyx -import pandas as pd # dependency for icepyx? - geopandas -import os -from .dataset import * - -class Argo_bgc(DataSet): - -# Argo data object to search/download (in one function) for BGC Argo data. - -%spatial_extent : list or string -# Spatial extent of interest, provided as a bounding box, list of polygon coordinates, or -# geospatial polygon file. -# Bounding box coordinates should be provided in decimal degrees as -# [lower-left-longitude, lower-left-latitute, upper-right-longitude, upper-right-latitude]. -# Polygon coordinates should be provided as coordinate pairs in decimal degrees as -# [(longitude1, latitude1), (longitude2, latitude2), ... (longitude_n,latitude_n), (longitude1,latitude1)] -# or -# [longitude1, latitude1, longitude2, latitude2, ... longitude_n,latitude_n, longitude1,latitude1]. - -% timeframe: list (2) of start date and end date, in YYYY-MM-DD - -% meas1, meas2 = string listing of argo measurement, e.g., bbp700, chla, temp, psal, doxy - - def __init__(self, shape, timeframe, meas1, meas2, presRange=None): - self.shape = shape - self.bounding_box = shape.extent # call coord standardization method (see icepyx) - self.time_frame = timeframe # call fmt_timerange - self.meas1 = meas1 - self.meas2 = meas2 - self.presrng = presRange - - def download(self, out_path): - baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection' - meas1Query = '?meas_1=' + self.meas1 - meas2Query = '&meas_2=' + self.meas2 - startDateQuery = '&startDate=' + self.time_frame[0].strftime('%Y-%m-%d') - endDateQuery = '&endDate=' + self.time_frame[1].strftime('%Y-%m-%d') - - shapeQuery = '&shape=' + self.shape # might have to process this - if not self.presrng == None: - pressRangeQuery = '&presRange=' + self.presrng - url = baseURL + meas1Query + meas2Query + startDateQuery + endDateQuery + pressRangeQuery + '&bgcOnly=true' + shapeQuery - else: - url = baseURL + meas1Query + meas2Query + startDateQuery + endDateQuery + '&bgcOnly=true' + shapeQuery - resp = requests.get(url) - - # Consider any status other than 2xx an error - if not resp.status_code // 100 == 2: - return "Error: Unexpected response {}".format(resp) - selectionProfiles = resp.json() - - # save selection profiles somewhere - # return selectionProfiles - - # ---------------------------------------------------------------------- - # Properties - - @property - def dataset(self): - """ - Return the short name dataset ID string associated with the query object. - - """ - return self._dset - - @property - def spatial_extent(self): - """ - Return an array showing the spatial extent of the query object. - Spatial extent is returned as an input type (which depends on how - you initially entered your spatial data) followed by the geometry data. - Bounding box data is [lower-left-longitude, lower-left-latitute, upper-right-longitude, upper-right-latitude]. - Polygon data is [[array of longitudes],[array of corresponding latitudes]]. - - """ - - if self.extent_type == "bounding_box": - return ["bounding box", self._spat_extent] - elif self.extent_type == "polygon": - # return ['polygon', self._spat_extent] - # Note: self._spat_extent is a shapely geometry object - return ["polygon", self._spat_extent.exterior.coords.xy] - else: - return ["unknown spatial type", None] - - @property - def dates(self): - """ - Return an array showing the date range of the query object. - Dates are returned as an array containing the start and end datetime objects, inclusive, in that order. - - """ - return [ - self._start.strftime("%Y-%m-%d"), - self._end.strftime("%Y-%m-%d"), - ] # could also use self._start.date() - - @property - def start_time(self): - """ - Return the start time specified for the start date. - NOTE THAT there is no time input for Argo - """ - return self._start.strftime("%H:%M:%S") - - @property - def end_time(self): - """ - Return the end time specified for the end date. - Examples - """ - return self._end.strftime("%H:%M:%S") - -## DO WE NEED AN ORDER VARS CLASS? or the download already puts data into a dataframe .. maybe order vars could -# save data to CSV? -# IF SO, this code may be relevant for it - -tick1 = 0 -tick2 = 0 -for index, value in enumerate(selectionProfiles): - if meas1 not in value['bgcMeasKeys']: - tick1 += 1 - if meas2 not in value['bgcMeasKeys']: - tick2 += 1 -if tick1 == len(selectionProfiles): - print(f'{meas1} not found in selected data') -if tick2 == len(selectionProfiles): - print(f'{meas2} not found in selected data') - -if tick1 < len(selectionProfiles) & tick2 < len(selectionProfiles): - df = json2dataframe(selectionProfiles, measKey='bgcMeas') - -df.head() - -# NEED TO ADD CODE for the visualization here -def visualize_spatial_extent( - self, - ): # additional args, basemap, zoom level, cmap, export - """ - Creates a map displaying the input spatial extent - Examples - -------- - >>> icepyx.query.Query('ATL06','path/spatialfile.shp',['2019-02-22','2019-02-28']) - >>> reg_a.visualize_spatial_extent - [visual map output] - """ - - world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) - f, ax = plt.subplots(1, figsize=(12, 6)) - world.plot(ax=ax, facecolor="lightgray", edgecolor="gray") - geospatial.geodataframe(self.extent_type, self._spat_extent).plot( - ax=ax, color="#FF8C00", alpha=0.7 - ) - plt.show() \ No newline at end of file diff --git a/get_BGC_argo.py b/get_BGC_argo.py deleted file mode 100644 index cc30b986e..000000000 --- a/get_BGC_argo.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Thu Oct 29 13:50:54 2020 - -@author: bissonk -""" - -# made for Python 3. It may work with Python 2.7, but has not been well tested - -# libraries to call for all python API calls on Argovis - -import requests -import pandas as pd -import os - -##### -# Get current directory to save file into - -curDir = os.getcwd() - -# Get a selected region from Argovis - - -def get_selection_profiles(startDate, endDate, shape, meas1,meas2, presRange=None): - baseURL = 'https://argovis.colorado.edu/selection/bgc_data_selection' - meas1Query = '?meas_1=' + meas1 - meas2Query = '&meas_2=' + meas2 - startDateQuery = '&startDate=' + startDate - endDateQuery = '&endDate=' + endDate - shapeQuery = '&shape='+shape - if not presRange == None: - pressRangeQuery = '&presRange=' + presRange - url = baseURL + meas1Query + meas2Query + startDateQuery + endDateQuery + pressRangeQuery + '&bgcOnly=true' + shapeQuery - else: - url = baseURL + meas1Query + meas2Query + startDateQuery + endDateQuery + '&bgcOnly=true' + shapeQuery - resp = requests.get(url) - # Consider any status other than 2xx an error - if not resp.status_code // 100 == 2: - return "Error: Unexpected response {}".format(resp) - selectionProfiles = resp.json() - return selectionProfiles - - -def json2dataframe(selectionProfiles, measKey='measurements'): - """ convert json data to Pandas DataFrame """ - # Make sure we deal with a list - if isinstance(selectionProfiles, list): - data = selectionProfiles - else: - data = [selectionProfiles] - # Transform - rows = [] - for profile in data: - keys = [x for x in profile.keys() if x not in ['measurements', 'bgcMeas']] - meta_row = dict((key, profile[key]) for key in keys) - for row in profile[measKey]: - row.update(meta_row) - rows.append(row) - df = pd.DataFrame(rows) - return df -# set start date, end date, lat/lon coordinates for the shape of region and pres range - -startDate='2020-10-08' -endDate='2020-10-22' -# shape should be nested array with lon, lat coords. -shape = '[[[-49.21875,48.806863],[-55.229808,54.85326],[-63.28125,60.500525],[-60.46875,64.396938],[-49.746094,61.185625],[-38.496094,54.059388],[-41.484375,47.754098],[-49.21875,48.806863]]]' -presRange='[0,30]' -meas1 = 'bbp700' -meas2 = 'chla' - -meas1= 'temp' -meas2='psal' -# tested with meas1 = temp, meas2 = psal and it works - -selectionProfiles = get_selection_profiles(startDate, endDate, shape, meas1, meas2, presRange=None) - - -# loop thru profiles and search for measurement -tick1 = 0 -tick2 = 0 -for index, value in enumerate(selectionProfiles): - if meas1 not in value['bgcMeasKeys']: - tick1 += 1 - if meas2 not in value['bgcMeasKeys']: - tick2 += 1 -if tick1 == len(selectionProfiles): - print(f'{meas1} not found in selected data') -if tick2 == len(selectionProfiles): - print(f'{meas2} not found in selected data') - -if tick1 < len(selectionProfiles) & tick2 < len(selectionProfiles): - df = json2dataframe(selectionProfiles, measKey='bgcMeas') - -df.head() - diff --git a/icepyx/quest/dataset_scripts/BGCargo.py b/icepyx/quest/dataset_scripts/BGCargo.py deleted file mode 100644 index d498da867..000000000 --- a/icepyx/quest/dataset_scripts/BGCargo.py +++ /dev/null @@ -1,216 +0,0 @@ -from icepyx.quest.dataset_scripts.dataset import DataSet -from icepyx.quest.dataset_scripts.argo import Argo -from icepyx.core.geospatial import geodataframe -import requests -import pandas as pd -import os -import numpy as np - - -class BGC_Argo(Argo): - def __init__(self, boundingbox, timeframe): - super().__init__(boundingbox, timeframe) - # self.profiles = None - - def _search_data_BGC_helper(self): - """ - make request with two params, and identify profiles that contain - remaining params - i.e. mandates the intersection of all specified params - """ - pass - - def search_data(self, params, presRange=None, printURL=False, keep_all=True): - - # assert len(params) != 0, 'One or more BGC measurements must be specified.' - - # API request requires exactly 2 measurement params, duplicate single of necessary - if len(params) == 1: - params.append(params[0]) - - # # validate list of user-entered params, sorts into order to be queried - # params = self._validate_parameters(params) - - # builds URL to be submitted - baseURL = "https://argovis.colorado.edu/selection/bgc_data_selection/" - - payload = { - "startDate": self._start.strftime("%Y-%m-%d"), - "endDate": self._end.strftime("%Y-%m-%d"), - "shape": [self._fmt_coordinates()], - "meas_1": params[0], - "meas_2": params[1], - } - - # if presRange: - # payload['presRange'] = presRange - - # # submit request - # resp = requests.get(baseURL, params=payload) - - # if printURL: - # print(resp.url) - - # # Consider any status other than 2xx an error - # if not resp.status_code // 100 == 2: - # msg = "Error: Unexpected response {}".format(resp) - # print(msg) - # return - - # selectionProfiles = resp.json() - - # # check for the existence of profiles from query - # if selectionProfiles == []: - # msg = 'Warning: Query returned no profiles\n' \ - # 'Please try different search parameters' - # print(msg) - # return - - # # deterine which profiles contain all specified params - # prof_ids = self._filter_profiles(selectionProfiles, params) - - # print('{0} valid profiles have been identified'.format(len(prof_ids))) - # iterate and download profiles individually - # for i in prof_ids: - # print("processing profile", i) - # self.download_by_profile(i) - - # self.profiles.reset_index(inplace=True) - - # if not keep_all: - # # drop BGC measurement columns not specified by user - # drop_params = list(set(list(self._valid_BGC_params())[3:]) - set(params)) - # qc_params = [] - # for i in drop_params: - # qc_params.append(i + '_qc') - # drop_params += qc_params - # self.profiles.drop(columns=drop_params, inplace=True, errors='ignore') - - def _valid_BGC_params(self): - """ - This is a list of valid BGC params, stored here to remove redundancy - They are ordered by how commonly they are measured (approx) - """ - params = valid_params = { - "pres": 0, - "temp": 1, - "psal": 2, - "cndx": 3, - "doxy": 4, - "ph_in_situ_total": 5, - "chla": 6, - "cdom": 7, - "nitrate": 8, - "bbp700": 9, - "down_irradiance412": 10, - "down_irradiance442": 11, - "down_irradiance490": 12, - "down_irradiance380": 13, - "downwelling_par": 14, - } - return params - - # def _validate_parameters(self, params): - # ''' - # Asserts that user-specified parameters are valid as per the Argovis documentation here: - # https://argovis.colorado.edu/api-docs/#/catalog/get_catalog_bgc_platform_data__platform_number_ - - # Returns - # ------- - # the list of params sorted in the order in which they should be queried (least - # commonly available to most commonly available) - # ''' - - # # valid params ordered by how commonly they are measured (approx) - # valid_params = self._valid_BGC_params() - - # # checks that params are valid - # for i in params: - # assert i in valid_params.keys(), \ - # "Parameter '{0}' is not valid. Valid parameters are {1}".format(i, valid_params.keys()) - - # # sorts params into order in which they should be queried - # params = sorted(params, key= lambda i: valid_params[i], reverse=True) - # return params - - def _filter_profiles(self, profiles, params): - """ - from a dictionary of all profiles returned by first API request, remove the - profiles that do not contain ALL BGC measurements specified by user - returns a list of profile ID's that contain all necessary BGC params - """ - # todo: filter out BGC profiles - good_profs = [] - for i in profiles: - bgc_meas = i["bgcMeasKeys"] - check = all(item in bgc_meas for item in params) - if check: - good_profs.append(i["_id"]) - # print(i['_id']) - - # profiles = good_profs - return good_profs - - # def download_by_profile(self, profile_number): - # url = 'https://argovis.colorado.edu/catalog/profiles/{}'.format(profile_number) - # resp = requests.get(url) - # # Consider any status other than 2xx an error - # if not resp.status_code // 100 == 2: - # return "Error: Unexpected response {}".format(resp) - # profile = resp.json() - # self._parse_into_df(profile) - # return profile - - def _parse_into_df(self, profiles): - """ - Stores profiles returned by query into dataframe - saves profiles back to self.profiles - returns None - """ - # todo: check that this makes appropriate BGC cols in the DF - # initialize dict - # meas_keys = profiles[0]['bgcMeasKeys'] - # df = pd.DataFrame(columns=meas_keys) - - if not isinstance(profiles, list): - profiles = [profiles] - - # initialise the df (empty or containing previously processed profiles) - if not self.profiles is None: - df = self.profiles - else: - df = pd.DataFrame() - - for profile in profiles: - profileDf = pd.DataFrame(profile["bgcMeas"]) - profileDf["cycle_number"] = profile["cycle_number"] - profileDf["profile_id"] = profile["_id"] - profileDf["lat"] = profile["lat"] - profileDf["lon"] = profile["lon"] - profileDf["date"] = profile["date"] - df = pd.concat([df, profileDf], sort=False) - # if self.profiles is None: - # df = pd.concat([df, profileDf], sort=False) - # else: - # df = df.merge(profileDf, on='profile_id') - self.profiles = df - - -if __name__ == "__main__": - # no profiles available - # reg_a = BGC_Argo([-154, 30, -143, 37], ['2022-04-12', '2022-04-26']) - # 24 profiles available - - reg_a = BGC_Argo([-150, 30, -120, 60], ["2022-06-07", "2022-06-21"]) - # reg_a.search_data(['doxy', 'nitrate', 'down_irradiance412'], printURL=True, keep_all=False) - reg_a.search_data(["down_irradiance412"], printURL=True, keep_all=False) - # print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) - - # reg_a.download_by_profile('4903026_101') - - # reg_a._validate_parameters(['doxy', - # 'chla', - # 'cdomm',]) - - # p = reg_a._validate_parameters(['nitrate', 'pres', 'doxy']) - # print(p) From a91c36074e1cf2de6e56179833c2e42c1197a25e Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 14 Aug 2023 11:54:59 -0400 Subject: [PATCH 062/124] remove bgc argo test file --- icepyx/tests/test_quest_BGCargo.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 icepyx/tests/test_quest_BGCargo.py diff --git a/icepyx/tests/test_quest_BGCargo.py b/icepyx/tests/test_quest_BGCargo.py deleted file mode 100644 index ceadb855f..000000000 --- a/icepyx/tests/test_quest_BGCargo.py +++ /dev/null @@ -1,16 +0,0 @@ -import icepyx as ipx -import pytest -import warnings - - -def test_available_profiles(): - pass - -def test_no_available_profiles(): - pass - -def test_valid_BGCparams(): - pass - -def test_invalid_BGCparams(): - pass \ No newline at end of file From cb367e178e7d9fcd8425af839afebb923e28dd5e Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 14 Aug 2023 11:56:27 -0400 Subject: [PATCH 063/124] update variables notebook from development --- doc/source/example_notebooks/IS2_data_variables.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/example_notebooks/IS2_data_variables.ipynb b/doc/source/example_notebooks/IS2_data_variables.ipynb index f821e8f3d..568027550 100644 --- a/doc/source/example_notebooks/IS2_data_variables.ipynb +++ b/doc/source/example_notebooks/IS2_data_variables.ipynb @@ -327,7 +327,7 @@ "metadata": {}, "outputs": [], "source": [ - "region_a.order_vars.append(beam_list=['gt1l', 'gt2l'], var_list=['latitude'])\n", + "var_dict = region_a.order_vars.append(beam_list=['gt1l', 'gt2l'], var_list=['latitude'])\n", "pprint(region_a.order_vars.wanted)" ] }, @@ -773,7 +773,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.10" + "version": "3.10.9" } }, "nbformat": 4, From 381092f5c93f0140255422307a0272f4a2703704 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 16 Aug 2023 15:31:47 -0400 Subject: [PATCH 064/124] simplify import statements --- icepyx/core/query.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/icepyx/core/query.py b/icepyx/core/query.py index b2041cbd4..5f0fa665f 100644 --- a/icepyx/core/query.py +++ b/icepyx/core/query.py @@ -12,12 +12,10 @@ import icepyx.core.APIformatting as apifmt import icepyx.core.granules as granules -from icepyx.core.granules import Granules as Granules +from granules import Granules import icepyx.core.is2ref as is2ref -# QUESTION: why doesn't from granules import Granules as Granules work, since granules=icepyx.core.granules? -# from icepyx.core.granules import Granules -from icepyx.core.variables import Variables as Variables +from icepyx.core.variables import Variables import icepyx.core.validate_inputs as val import icepyx.core.spatial as spat import icepyx.core.temporal as tp From 283748e9446737c4f27581bf67a3836a40b70bae Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Fri, 18 Aug 2023 18:32:49 +0000 Subject: [PATCH 065/124] quickfix for granules error --- icepyx/core/query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icepyx/core/query.py b/icepyx/core/query.py index 5f0fa665f..c643b390c 100644 --- a/icepyx/core/query.py +++ b/icepyx/core/query.py @@ -12,7 +12,7 @@ import icepyx.core.APIformatting as apifmt import icepyx.core.granules as granules -from granules import Granules +from icepyx.core.granules import Granules import icepyx.core.is2ref as is2ref from icepyx.core.variables import Variables From 789330788f26f3ce341d731a56695fd19e142c97 Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Fri, 18 Aug 2023 19:40:37 +0000 Subject: [PATCH 066/124] draft subpage on available QUEST datasets --- .../contributing/quest-available-datasets.rst | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 doc/source/contributing/quest-available-datasets.rst diff --git a/doc/source/contributing/quest-available-datasets.rst b/doc/source/contributing/quest-available-datasets.rst new file mode 100644 index 000000000..a44a70599 --- /dev/null +++ b/doc/source/contributing/quest-available-datasets.rst @@ -0,0 +1,23 @@ +QUEST Supported Datasets +======================== + +On this page, we outline the datasets that are supported by the QUEST module. Click on the links for each dataset to view information about the API and sensor/data platform used. + + +List of Datasets +---------------- + +* `Argo `_ + * The Argo mission involves a series of floats that are designed to capture vertical ocean profiles of temperature, salinity, and pressure down to ~2000 m. Some floats are in support of BGC-Argo, which also includes data relevant for biogeochemical applications: oxygen, nitrate, chlorophyll, backscatter, and solar irradiance. + * (Link Kelsey's paper here) + * (Link to example workbook here) + + +Adding a Dataset to QUEST +------------------------- + +Want to add a new dataset to QUEST? No problem! QUEST includes a template script (``dataset.py``) that may be used to create your own querying module for a dataset of interest. + +Guidelines on how to construct your dataset module may be found here: (link to be added) + +Once you have developed a script with the template, you may request for the module to be added to QUEST via Github. Please see the How to Contribute page :ref:`_dev_guide_label` for instructions on how to contribute to icepyx. \ No newline at end of file From 949ffee74672623b1f8e4c21873ba29ead7c70f7 Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Fri, 18 Aug 2023 19:43:57 +0000 Subject: [PATCH 067/124] small reference fix in text --- doc/source/contributing/quest-available-datasets.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/contributing/quest-available-datasets.rst b/doc/source/contributing/quest-available-datasets.rst index a44a70599..c6d3e7f97 100644 --- a/doc/source/contributing/quest-available-datasets.rst +++ b/doc/source/contributing/quest-available-datasets.rst @@ -20,4 +20,4 @@ Want to add a new dataset to QUEST? No problem! QUEST includes a template script Guidelines on how to construct your dataset module may be found here: (link to be added) -Once you have developed a script with the template, you may request for the module to be added to QUEST via Github. Please see the How to Contribute page :ref:`_dev_guide_label` for instructions on how to contribute to icepyx. \ No newline at end of file +Once you have developed a script with the template, you may request for the module to be added to QUEST via Github. Please see the How to Contribute page :ref:`dev_guide_label` for instructions on how to contribute to icepyx. \ No newline at end of file From 7414c850eb6e617e9798484e1d78ad586ad5cb49 Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Fri, 18 Aug 2023 19:56:46 +0000 Subject: [PATCH 068/124] add reference to top of .rst file --- doc/source/contributing/quest-available-datasets.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/contributing/quest-available-datasets.rst b/doc/source/contributing/quest-available-datasets.rst index c6d3e7f97..91a6283a0 100644 --- a/doc/source/contributing/quest-available-datasets.rst +++ b/doc/source/contributing/quest-available-datasets.rst @@ -1,3 +1,5 @@ +.. _quest_supported_label: + QUEST Supported Datasets ======================== From 63e1b5727c20bb2d5969597ca04e989210207027 Mon Sep 17 00:00:00 2001 From: Romina Date: Fri, 18 Aug 2023 20:25:26 -0400 Subject: [PATCH 069/124] test argo df merge --- icepyx/tests/test_argo.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/icepyx/tests/test_argo.py b/icepyx/tests/test_argo.py index 182b4727f..647296d12 100644 --- a/icepyx/tests/test_argo.py +++ b/icepyx/tests/test_argo.py @@ -23,5 +23,8 @@ def test_merge_df(): assert "down_irradiance412_argoqc" in df.columns -def test_abc(): - assert 1 == 1 \ No newline at end of file +def test_validate_params(): + + bad_param = ["up_irradiance412"] + + error_msg = Argo._validate_params(bad_param) \ No newline at end of file From b064224988f921a76de5851ba858065fd8f27b0d Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 30 Aug 2023 11:26:06 -0400 Subject: [PATCH 070/124] update argo script from shared_search branch --- icepyx/quest/dataset_scripts/argo.py | 197 +++++++++++++++++---------- 1 file changed, 122 insertions(+), 75 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index d0e7cf978..60343e125 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -37,86 +37,39 @@ class Argo(DataSet): """ # Note: it looks like ArgoVis now accepts polygons, not just bounding boxes - def __init__(self, boundingbox, timeframe): - super().__init__(boundingbox, timeframe) + def __init__(self, aoi, toi, params=["temperature"], presRange=None): + # super().__init__(boundingbox, timeframe) + self.params = self._validate_parameters(params) + self.presRange = presRange + self._spatial = aoi + self._temporal = toi + # todo: verify that this will only work with a bounding box (I think our code can accept arbitrary polygons) assert self._spatial._ext_type == "bounding_box" self.argodata = None self._apikey = "92259861231b55d32a9c0e4e3a93f4834fc0b6fa" - def search_data( - self, params=["temperature"], presRange=None, printURL=False - ) -> str: - """ - Query for available argo profiles given the spatio temporal criteria - and other params specific to the dataset. - - Parameters - --------- - params: list of str, default ["temperature"] - A list of strings, where each string is a requested parameter. - Only metadata for profiles with the requested parameters are returned. - To search for all parameters, use `params=["all"]`; - be careful using all for floats with BGC data, as this may be result in a large download. - presRange: str, default None - The pressure range (which correllates with depth) to search for data within. - Input as a "shallow-limit,deep-limit" string. Note the lack of space. - printURL: boolean, default False - Print the URL of the data request. Useful for debugging and when no data is returned. + def __str__(self): - Returns - ------ - str: message on the success status of the search - """ - - params = self._validate_parameters(params) - print(params) - - # builds URL to be submitted - baseURL = "https://argovis-api.colorado.edu/argo" - payload = { - "startDate": self._temporal._start.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), - "endDate": self._temporal._end.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), - "polygon": [self._fmt_coordinates()], - "data": params, - } - if presRange: - payload["presRange"] = presRange + if self.presRange is None: + prange = "All" + else: + prange = str(self.presRange) - # submit request - resp = requests.get( - baseURL, headers={"x-argokey": self._apikey}, params=payload + if self.argodata is None: + df = "No data yet" + else: + df = "\n" + str(self.argodata.head()) + s = ( + "---Argo---\n" + "Parameters: {0}\n" + "Pressure range: {1}\n" + "Dataframe head: {2}".format(self.params, prange, df) ) - if printURL: - print(resp.url) - - selectionProfiles = resp.json() - - # Consider any status other than 2xx an error - if not resp.status_code // 100 == 2: - # check for the existence of profiles from query - if selectionProfiles == []: - msg = ( - "Warning: Query returned no profiles\n" - "Please try different search parameters" - ) - print(msg) - return msg - - else: - msg = "Error: Unexpected response {}".format(resp) - print(msg) - return msg + return s - # record the profile ids for the profiles that contain the requested parameters - prof_ids = [] - for i in selectionProfiles: - prof_ids.append(i["_id"]) - self.prof_ids = prof_ids - - msg = "{0} valid profiles have been identified".format(len(prof_ids)) - print(msg) - return msg + # ---------------------------------------------------------------------- + # Formatting API Inputs def _fmt_coordinates(self) -> str: """ @@ -137,6 +90,9 @@ def _fmt_coordinates(self) -> str: x = "[" + x + "]" return x + # ---------------------------------------------------------------------- + # Validation + def _valid_params(self) -> list: """ A list of valid Argo measurement parameters (including BGC). @@ -243,6 +199,85 @@ def _validate_parameters(self, params) -> list: return params + # ---------------------------------------------------------------------- + # Querying and Getting Data + + def search_data(self, params=None, printURL=False) -> str: + """ + Query for available argo profiles given the spatio temporal criteria + and other params specific to the dataset. + + Parameters + --------- + params: list of str, default ["temperature"] + A list of strings, where each string is a requested parameter. + Only metadata for profiles with the requested parameters are returned. + To search for all parameters, use `params=["all"]`; + be careful using all for floats with BGC data, as this may be result in a large download. + presRange: str, default None + The pressure range (which correllates with depth) to search for data within. + Input as a "shallow-limit,deep-limit" string. Note the lack of space. + printURL: boolean, default False + Print the URL of the data request. Useful for debugging and when no data is returned. + + Returns + ------ + str: message on the success status of the search + """ + + # if new search is called with additional parameters + if not params is None: + self.params.append(self._validate_parameters(params)) + # to remove duplicated from list + self.params = list(set(self.params)) + + # builds URL to be submitted + baseURL = "https://argovis-api.colorado.edu/argo" + payload = { + "startDate": self._temporal._start.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "endDate": self._temporal._end.strftime("%Y-%m-%dT%H:%M:%S.%fZ"), + "polygon": [self._fmt_coordinates()], + "data": self.params, + } + if self.presRange: + payload["presRange"] = self.presRange + + # submit request + resp = requests.get( + baseURL, headers={"x-argokey": self._apikey}, params=payload + ) + + if printURL: + print(resp.url) + + selectionProfiles = resp.json() + + # Consider any status other than 2xx an error + if not resp.status_code // 100 == 2: + # check for the existence of profiles from query + if selectionProfiles == []: + msg = ( + "Warning: Query returned no profiles\n" + "Please try different search parameters" + ) + print(msg) + return msg + + else: + msg = "Error: Unexpected response {}".format(resp) + print(msg) + return msg + + # record the profile ids for the profiles that contain the requested parameters + prof_ids = [] + for i in selectionProfiles: + prof_ids.append(i["_id"]) + self.prof_ids = prof_ids + + msg = "{0} valid profiles have been identified".format(len(prof_ids)) + print(msg) + return msg + def _download_profile( self, profile_number, @@ -335,7 +370,7 @@ def _parse_into_df(self, profile_data) -> pd.DataFrame: return profileDf - def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFrame: + def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFrame: """ Downloads the requested data for a list of profile IDs (stored under .prof_ids) and returns it in a DataFrame. @@ -372,6 +407,18 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr "will be added to previously downloaded data.", ) + # if new search is called with additional parameters + if not params is None: + self.params.append(self._validate_parameters(params)) + # to remove duplicated from list + self.params = list(set(self.params)) + else: + params = self.params + + # if new search is called with new pressure range + if not presRange is None: + self.presRange = presRange + # Add qc data for each of the parameters requested if params == ["all"]: pass @@ -383,7 +430,7 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr params.append(p + "_argoqc") # intentionally resubmit search to reset prof_ids, in case the user requested different parameters - self.search_data(params, presRange=presRange) + self.search_data() # create a dataframe for each profile and merge it with the rest of the profiles from this set of parameters being downloaded merged_df = pd.DataFrame(columns=["profile_id"]) @@ -423,9 +470,9 @@ def get_dataframe(self, params, presRange=None, keep_existing=True) -> pd.DataFr # reg_a.search_data(params=bad_param, printURL=True) - reg_a.get_dataframe(params=param_list) + reg_a.download(params=param_list) - reg_a.get_dataframe(params=["doxy"], keep_existing=True) # , presRange="0.2,100" + reg_a.download(params=["doxy"], keep_existing=True) # , presRange="0.2,100" # ) print(reg_a) From 1d533417927d735f05882f82ef379898288c5ee4 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 25 Sep 2023 11:45:10 -0400 Subject: [PATCH 071/124] update QUEST and GenQuery classes for argo integration (#441) * Adding argo search and download script * Create get_argo.py Download the 'classic' argo data with physical variables only * begin implementing argo dataset * 1st draft implementing argo dataset * implement search_data for physical argo * doctests and general cleanup for physical argo query * beginning of BGC Argo download * parse BGC profiles into DF * plan to query BGC profiles * validate BGC param input function * order BGC params in order in which they should be queried * fix bug in parse_into_df() - init blank df to take in union of params from all profiles * identify profiles from initial API request containing all required params * creates df with only profiles that contain all user specified params Need to dload additional params * modified to populate prof df by querying individual profiles * finished up BGC argo download! * assert bounding box type in Argo init, begin framework for unit tests * Adding argo search and download script * Create get_argo.py Download the 'classic' argo data with physical variables only * begin implementing argo dataset * 1st draft implementing argo dataset * implement search_data for physical argo * doctests and general cleanup for physical argo query * beginning of BGC Argo download * parse BGC profiles into DF * plan to query BGC profiles * validate BGC param input function * order BGC params in order in which they should be queried * fix bug in parse_into_df() - init blank df to take in union of params from all profiles * identify profiles from initial API request containing all required params * creates df with only profiles that contain all user specified params Need to dload additional params * modified to populate prof df by querying individual profiles * finished up BGC argo download! * assert bounding box type in Argo init, begin framework for unit tests * need to confirm spatial extent is bbox * begin test case for available profiles * add tests for argo.py * add typing, add example json, and use it to test parsing * update argo to submit successful api request (update keys and values submitted) * first pass at porting argo over to metadata+per profile download (WIP) * basic working argo script * simplify parameter validation (ordered list no longer needed) * add option to delete existing data before new download * continue cleaning up argo.py * fix download_by_profile to properly store all downloaded data * remove old get_argo.py script * remove _filter_profiles function in favor of submitting data kwarg in request * start filling in docstrings * clean up nearly duplicate functions * add more docstrings * get a few minimal argo tests working * add bgc argo params. begin adding merge for second download runs * some changes * WIP test commit to see if can push to GH * WIP handling argo merge issue * update profile to df to return df and move merging to get_dataframe * merge profiles with existing df * clean up docstrings and code * add test_argo.py * add prelim test case for adding to Argo df * remove sandbox files * remove bgc argo test file * update variables notebook from development * simplify import statements * quickfix for granules error * draft subpage on available QUEST datasets * small reference fix in text * add reference to top of .rst file * test argo df merge * add functionality to Quest class to pass search criteria to all datasets * add functionality to Quest class to pass search criteria to all datasets * update dataset docstrings; reorder argo.py to match * implement quest search+download for IS2 * move spatial and temporal properties from query to genquery * add query docstring test for cycles,tracks to test file * add quest test module * standardize print outputs for quest search and download; is2 download needs auth updates * remove extra files from this branch * comment out argo portions of quest for PR * remove argo-branch-only init file * remove argo script from branch * remove argo test file from branch * comment out another line of argo stuff * Update quest.py Added Docstrings to functions within quest.py and edited the primary docstring for the QUEST class here. Note I did not add Docstrings to the implicit __self__ function. * Update test_quest.py Added comments (not Docstrings) to test functions * Update dataset.py Minor edits to the doc strings * Update quest.py Edited docstrings * catch error with downloading datasets in Quest; template test case for multi dataset query --------- Co-authored-by: Kelsey Bisson <48059682+kelseybisson@users.noreply.github.com> Co-authored-by: Romina Co-authored-by: zachghiaccio Co-authored-by: Zach Fair <48361714+zachghiaccio@users.noreply.github.com> --- .../contributing/quest-available-datasets.rst | 25 ++ icepyx/core/query.py | 345 +++++++++--------- icepyx/quest/__init__.py | 0 icepyx/quest/dataset_scripts/dataset.py | 90 +++-- icepyx/quest/quest.py | 104 +++++- icepyx/tests/test_query.py | 12 + icepyx/tests/test_quest.py | 80 ++++ 7 files changed, 424 insertions(+), 232 deletions(-) create mode 100644 doc/source/contributing/quest-available-datasets.rst delete mode 100644 icepyx/quest/__init__.py create mode 100644 icepyx/tests/test_quest.py diff --git a/doc/source/contributing/quest-available-datasets.rst b/doc/source/contributing/quest-available-datasets.rst new file mode 100644 index 000000000..91a6283a0 --- /dev/null +++ b/doc/source/contributing/quest-available-datasets.rst @@ -0,0 +1,25 @@ +.. _quest_supported_label: + +QUEST Supported Datasets +======================== + +On this page, we outline the datasets that are supported by the QUEST module. Click on the links for each dataset to view information about the API and sensor/data platform used. + + +List of Datasets +---------------- + +* `Argo `_ + * The Argo mission involves a series of floats that are designed to capture vertical ocean profiles of temperature, salinity, and pressure down to ~2000 m. Some floats are in support of BGC-Argo, which also includes data relevant for biogeochemical applications: oxygen, nitrate, chlorophyll, backscatter, and solar irradiance. + * (Link Kelsey's paper here) + * (Link to example workbook here) + + +Adding a Dataset to QUEST +------------------------- + +Want to add a new dataset to QUEST? No problem! QUEST includes a template script (``dataset.py``) that may be used to create your own querying module for a dataset of interest. + +Guidelines on how to construct your dataset module may be found here: (link to be added) + +Once you have developed a script with the template, you may request for the module to be added to QUEST via Github. Please see the How to Contribute page :ref:`dev_guide_label` for instructions on how to contribute to icepyx. \ No newline at end of file diff --git a/icepyx/core/query.py b/icepyx/core/query.py index e8f1d8e7c..3459fd132 100644 --- a/icepyx/core/query.py +++ b/icepyx/core/query.py @@ -12,11 +12,9 @@ import icepyx.core.APIformatting as apifmt from icepyx.core.auth import EarthdataAuthMixin import icepyx.core.granules as granules -from icepyx.core.granules import Granules as Granules +# QUESTION: why doesn't from granules import Granules work, since granules=icepyx.core.granules? +from icepyx.core.granules import Granules import icepyx.core.is2ref as is2ref - -# QUESTION: why doesn't from granules import Granules as Granules work, since granules=icepyx.core.granules? -# from icepyx.core.granules import Granules import icepyx.core.spatial as spat import icepyx.core.temporal as tp import icepyx.core.validate_inputs as val @@ -148,6 +146,177 @@ def __str__(self): ) return str + # ---------------------------------------------------------------------- + # Properties + + @property + def temporal(self): + """ + Return the Temporal object containing date/time range information for the query object. + + See Also + -------- + temporal.Temporal.start + temporal.Temporal.end + temporal.Temporal + + Examples + -------- + >>> reg_a = GenQuery([-55, 68, -48, 71],['2019-02-20','2019-02-28']) + >>> print(reg_a.temporal) + Start date and time: 2019-02-20 00:00:00 + End date and time: 2019-02-28 23:59:59 + + >>> reg_a = GenQuery([-55, 68, -48, 71],cycles=['03','04','05','06','07'], tracks=['0849','0902']) + >>> print(reg_a.temporal) + ['No temporal parameters set'] + """ + + if hasattr(self, "_temporal"): + return self._temporal + else: + return ["No temporal parameters set"] + + @property + def spatial(self): + """ + Return the spatial object, which provides the underlying functionality for validating + and formatting geospatial objects. The spatial object has several properties to enable + user access to the stored spatial extent in multiple formats. + + See Also + -------- + spatial.Spatial.spatial_extent + spatial.Spatial.extent_type + spatial.Spatial.extent_file + spatial.Spatial + + Examples + -------- + >>> reg_a = ipx.GenQuery([-55, 68, -48, 71],['2019-02-20','2019-02-28']) + >>> reg_a.spatial # doctest: +SKIP + + + >>> print(reg_a.spatial) + Extent type: bounding_box + Coordinates: [-55.0, 68.0, -48.0, 71.0] + + """ + return self._spatial + + @property + def spatial_extent(self): + """ + Return an array showing the spatial extent of the query object. + Spatial extent is returned as an input type (which depends on how + you initially entered your spatial data) followed by the geometry data. + Bounding box data is [lower-left-longitude, lower-left-latitute, upper-right-longitude, upper-right-latitude]. + Polygon data is [longitude1, latitude1, longitude2, latitude2, + ... longitude_n,latitude_n, longitude1,latitude1]. + + Returns + ------- + tuple of length 2 + First tuple element is the spatial type ("bounding box" or "polygon"). + Second tuple element is the spatial extent as a list of coordinates. + + Examples + -------- + + # Note: coordinates returned as float, not int + >>> reg_a = GenQuery([-55, 68, -48, 71],['2019-02-20','2019-02-28']) + >>> reg_a.spatial_extent + ('bounding_box', [-55.0, 68.0, -48.0, 71.0]) + + >>> reg_a = GenQuery([(-55, 68), (-55, 71), (-48, 71), (-48, 68), (-55, 68)],['2019-02-20','2019-02-28']) + >>> reg_a.spatial_extent + ('polygon', [-55.0, 68.0, -55.0, 71.0, -48.0, 71.0, -48.0, 68.0, -55.0, 68.0]) + + # NOTE Is this where we wanted to put the file-based test/example? + # The test file path is: examples/supporting_files/simple_test_poly.gpkg + + See Also + -------- + Spatial.extent + Spatial.extent_type + Spatial.extent_as_gdf + + """ + + return (self._spatial._ext_type, self._spatial._spatial_ext) + + @property + def dates(self): + """ + Return an array showing the date range of the query object. + Dates are returned as an array containing the start and end datetime objects, inclusive, in that order. + + Examples + -------- + >>> reg_a = ipx.GenQuery([-55, 68, -48, 71],['2019-02-20','2019-02-28']) + >>> reg_a.dates + ['2019-02-20', '2019-02-28'] + + >>> reg_a = GenQuery([-55, 68, -48, 71]) + >>> reg_a.dates + ['No temporal parameters set'] + """ + if not hasattr(self, "_temporal"): + return ["No temporal parameters set"] + else: + return [ + self._temporal._start.strftime("%Y-%m-%d"), + self._temporal._end.strftime("%Y-%m-%d"), + ] # could also use self._start.date() + + @property + def start_time(self): + """ + Return the start time specified for the start date. + + Examples + -------- + >>> reg_a = ipx.GenQuery([-55, 68, -48, 71],['2019-02-20','2019-02-28']) + >>> reg_a.start_time + '00:00:00' + + >>> reg_a = ipx.GenQuery([-55, 68, -48, 71],['2019-02-20','2019-02-28'], start_time='12:30:30') + >>> reg_a.start_time + '12:30:30' + + >>> reg_a = GenQuery([-55, 68, -48, 71]) + >>> reg_a.start_time + ['No temporal parameters set'] + """ + if not hasattr(self, "_temporal"): + return ["No temporal parameters set"] + else: + return self._temporal._start.strftime("%H:%M:%S") + + @property + def end_time(self): + """ + Return the end time specified for the end date. + + Examples + -------- + >>> reg_a = ipx.GenQuery([-55, 68, -48, 71],['2019-02-20','2019-02-28']) + >>> reg_a.end_time + '23:59:59' + + >>> reg_a = ipx.GenQuery([-55, 68, -48, 71],['2019-02-20','2019-02-28'], end_time='10:20:20') + >>> reg_a.end_time + '10:20:20' + + >>> reg_a = GenQuery([-55, 68, -48, 71]) + >>> reg_a.end_time + ['No temporal parameters set'] + """ + if not hasattr(self, "_temporal"): + return ["No temporal parameters set"] + else: + return self._temporal._end.strftime("%H:%M:%S") + # DevGoal: update docs throughout to allow for polygon spatial extent # Note: add files to docstring once implemented @@ -333,174 +502,6 @@ def product_version(self): """ return self._version - @property - def temporal(self): - """ - Return the Temporal object containing date/time range information for the query object. - - See Also - -------- - temporal.Temporal.start - temporal.Temporal.end - temporal.Temporal - - Examples - -------- - >>> reg_a = Query('ATL06',[-55, 68, -48, 71],['2019-02-20','2019-02-28']) - >>> print(reg_a.temporal) - Start date and time: 2019-02-20 00:00:00 - End date and time: 2019-02-28 23:59:59 - - >>> reg_a = Query('ATL06',[-55, 68, -48, 71],cycles=['03','04','05','06','07'], tracks=['0849','0902']) - >>> print(reg_a.temporal) - ['No temporal parameters set'] - """ - - if hasattr(self, "_temporal"): - return self._temporal - else: - return ["No temporal parameters set"] - - @property - def spatial(self): - """ - Return the spatial object, which provides the underlying functionality for validating - and formatting geospatial objects. The spatial object has several properties to enable - user access to the stored spatial extent in multiple formats. - - See Also - -------- - spatial.Spatial.spatial_extent - spatial.Spatial.extent_type - spatial.Spatial.extent_file - spatial.Spatial - - Examples - -------- - >>> reg_a = ipx.Query('ATL06',[-55, 68, -48, 71],['2019-02-20','2019-02-28']) - >>> reg_a.spatial # doctest: +SKIP - - - >>> print(reg_a.spatial) - Extent type: bounding_box - Coordinates: [-55.0, 68.0, -48.0, 71.0] - - """ - return self._spatial - - @property - def spatial_extent(self): - """ - Return an array showing the spatial extent of the query object. - Spatial extent is returned as an input type (which depends on how - you initially entered your spatial data) followed by the geometry data. - Bounding box data is [lower-left-longitude, lower-left-latitute, upper-right-longitude, upper-right-latitude]. - Polygon data is [longitude1, latitude1, longitude2, latitude2, - ... longitude_n,latitude_n, longitude1,latitude1]. - - Returns - ------- - tuple of length 2 - First tuple element is the spatial type ("bounding box" or "polygon"). - Second tuple element is the spatial extent as a list of coordinates. - - Examples - -------- - - # Note: coordinates returned as float, not int - >>> reg_a = Query('ATL06',[-55, 68, -48, 71],['2019-02-20','2019-02-28']) - >>> reg_a.spatial_extent - ('bounding_box', [-55.0, 68.0, -48.0, 71.0]) - - >>> reg_a = Query('ATL06',[(-55, 68), (-55, 71), (-48, 71), (-48, 68), (-55, 68)],['2019-02-20','2019-02-28']) - >>> reg_a.spatial_extent - ('polygon', [-55.0, 68.0, -55.0, 71.0, -48.0, 71.0, -48.0, 68.0, -55.0, 68.0]) - - # NOTE Is this where we wanted to put the file-based test/example? - # The test file path is: examples/supporting_files/simple_test_poly.gpkg - - See Also - -------- - Spatial.extent - Spatial.extent_type - Spatial.extent_as_gdf - - """ - - return (self._spatial._ext_type, self._spatial._spatial_ext) - - @property - def dates(self): - """ - Return an array showing the date range of the query object. - Dates are returned as an array containing the start and end datetime objects, inclusive, in that order. - - Examples - -------- - >>> reg_a = ipx.Query('ATL06',[-55, 68, -48, 71],['2019-02-20','2019-02-28']) - >>> reg_a.dates - ['2019-02-20', '2019-02-28'] - - >>> reg_a = Query('ATL06',[-55, 68, -48, 71],cycles=['03','04','05','06','07'], tracks=['0849','0902']) - >>> reg_a.dates - ['No temporal parameters set'] - """ - if not hasattr(self, "_temporal"): - return ["No temporal parameters set"] - else: - return [ - self._temporal._start.strftime("%Y-%m-%d"), - self._temporal._end.strftime("%Y-%m-%d"), - ] # could also use self._start.date() - - @property - def start_time(self): - """ - Return the start time specified for the start date. - - Examples - -------- - >>> reg_a = ipx.Query('ATL06',[-55, 68, -48, 71],['2019-02-20','2019-02-28']) - >>> reg_a.start_time - '00:00:00' - - >>> reg_a = ipx.Query('ATL06',[-55, 68, -48, 71],['2019-02-20','2019-02-28'], start_time='12:30:30') - >>> reg_a.start_time - '12:30:30' - - >>> reg_a = Query('ATL06',[-55, 68, -48, 71],cycles=['03','04','05','06','07'], tracks=['0849','0902']) - >>> reg_a.start_time - ['No temporal parameters set'] - """ - if not hasattr(self, "_temporal"): - return ["No temporal parameters set"] - else: - return self._temporal._start.strftime("%H:%M:%S") - - @property - def end_time(self): - """ - Return the end time specified for the end date. - - Examples - -------- - >>> reg_a = ipx.Query('ATL06',[-55, 68, -48, 71],['2019-02-20','2019-02-28']) - >>> reg_a.end_time - '23:59:59' - - >>> reg_a = ipx.Query('ATL06',[-55, 68, -48, 71],['2019-02-20','2019-02-28'], end_time='10:20:20') - >>> reg_a.end_time - '10:20:20' - - >>> reg_a = Query('ATL06',[-55, 68, -48, 71],cycles=['03','04','05','06','07'], tracks=['0849','0902']) - >>> reg_a.end_time - ['No temporal parameters set'] - """ - if not hasattr(self, "_temporal"): - return ["No temporal parameters set"] - else: - return self._temporal._end.strftime("%H:%M:%S") - @property def cycles(self): """ diff --git a/icepyx/quest/__init__.py b/icepyx/quest/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index 13e926229..e76081e08 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -1,4 +1,5 @@ import warnings +from icepyx.core.query import GenQuery warnings.filterwarnings("ignore") @@ -6,78 +7,75 @@ class DataSet: """ - Parent Class for all supported datasets (i.e. ATL03, ATL07, MODIS, etc.) - all sub classes must support the following methods for use in - colocated data class + Template parent class for all QUEST supported datasets (i.e. ICESat-2, Argo BGC, Argo, MODIS, etc.). + All sub-classes must support the following methods for use via the QUEST class. """ - def __init__(self, boundingbox, timeframe): + def __init__( + self, spatial_extent=None, date_range=None, start_time=None, end_time=None + ): """ - * use existing Icepyx functionality to initialise this - :param timeframe: datetime + Complete any dataset specific initializations (i.e. beyond space and time) required here. + For instance, ICESat-2 requires a product, and Argo requires parameters. + One can also check that the "default" space and time supplied by QUEST are the right format + (e.g. if the spatial extent must be a bounding box). """ - self.bounding_box = boundingbox - self.time_frame = timeframe - - def _fmt_coordinates(self): - # use icepyx geospatial module (icepyx core) raise NotImplementedError - def _fmt_timerange(self): + # ---------------------------------------------------------------------- + # Formatting API Inputs + + def _fmt_coordinates(self): """ - will return list of datetime objects [start_time, end_time] + Convert spatial extent into format needed by DataSet API, + if different than the formats available directly from SuperQuery. """ raise NotImplementedError - # todo: merge with Icepyx SuperQuery - def _validate_input(self): + def _fmt_timerange(self): """ - This may already be done in icepyx. - Not sure if we need this here + Convert temporal information into format needed by DataSet API, + if different than the formats available directly from SuperQuery. """ raise NotImplementedError - def search_data(self, delta_t): + # ---------------------------------------------------------------------- + # Validation + + def _validate_inputs(self): """ - query dataset given the spatio temporal criteria - and other params specic to the dataset + Create any additional validation functions for verifying inputs. + This function is not explicitly called by QUEST, + but is frequently needed for preparing API requests. + + See Also + -------- + quest.dataset_scripts.argo.Argo._validate_parameters """ raise NotImplementedError - def download(self, out_path): + # ---------------------------------------------------------------------- + # Querying and Getting Data + + def search_data(self): """ - once data is querried, the user may choose to dowload the - data locally + Query the dataset (i.e. search for available data) + given the spatiotemporal criteria and other parameters specific to the dataset. """ raise NotImplementedError - def visualize(self): + def download(self): """ - (once data is downloaded)?, makes a quick plot showing where - data are located - e.g. Plots location of Argo profile or highlights ATL03 photon track + Download the data to your local machine. """ raise NotImplementedError - def _add2colocated_plot(self): + # ---------------------------------------------------------------------- + # Working with Data + + def visualize(self): """ - Takes visualise() functionality and adds the plot to central - plot with other coincident data. This will be called by - show_area_overlap() in Colocateddata class + Tells QUEST how to plot data (for instance, which parameters to plot) on a basemap. + For ICESat-2, it might show a photon track, and for Argo it might show a profile location. """ raise NotImplementedError - - """ - The following are low priority functions - Not sure these are even worth keeping. Doesn't make sense for - all datasets. - """ - - # def get_meltpond_fraction(self): - # raise NotImplementedError - # - # def get_sea_ice_fraction(self): - # raise NotImplementedError - # - # def get_roughness(self): - # raise NotImplementedError diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index 2855a879c..c54e49b73 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -1,25 +1,26 @@ import matplotlib.pyplot as plt -from icepyx.core.query import GenQuery +from icepyx.core.query import GenQuery, Query + +# from icepyx.quest.dataset_scripts.argo import Argo # todo: implement the subclass inheritance class Quest(GenQuery): """ QUEST - Query Unify Explore SpatioTemporal - object to query, obtain, and perform basic - operations on datasets for combined analysis with ICESat-2 data products. - A new dataset can be added using the `dataset.py` template. - A list of already supported datasets is available at: - Expands the icepyx GenQuery superclass. + operations on datasets (i.e. Argo, BGC Argo, MODIS, etc) for combined analysis with ICESat-2 + data products. A new dataset can be added using the `dataset.py` template. + QUEST expands the icepyx GenQuery superclass. See the doc page for GenQuery for details on temporal and spatial input parameters. Parameters ---------- - projection : proj4 string - Not yet implemented - Ex text: a string name of projection to be used for plotting (e.g. 'Mercator', 'NorthPolarStereographic') + proj : proj4 string + Geospatial projection. + Not yet implemented Returns ------- @@ -38,7 +39,6 @@ class Quest(GenQuery): Date range: (2019-02-20 00:00:00, 2019-02-28 23:59:59) Data sets: None - # todo: make this work with real datasets Add datasets to the quest object. >>> reg_a.datasets = {'ATL07':None, 'Argo':None} @@ -61,13 +61,11 @@ def __init__( end_time=None, proj="Default", ): + """ + Tells QUEST to initialize data given the user input spatiotemporal data. + """ super().__init__(spatial_extent, date_range, start_time, end_time) self.datasets = {} - self.projection = self._determine_proj(proj) - - # todo: maybe move this to icepyx superquery class - def _determine_proj(self, proj): - return None def __str__(self): str = super(Quest, self).__str__() @@ -83,4 +81,82 @@ def __str__(self): return str + # ---------------------------------------------------------------------- + # Datasets + + def add_icesat2( + self, + product=None, + start_time=None, + end_time=None, + version=None, + cycles=None, + tracks=None, + files=None, + **kwargs, + ): + """ + Adds ICESat-2 datasets to QUEST structure. + """ + + query = Query( + product, + self._spatial.extent, + [self._temporal.start, self._temporal.end], + start_time, + end_time, + version, + cycles, + tracks, + files, + **kwargs, + ) + + self.datasets["icesat2"] = query + + # def add_argo(self, params=["temperature"], presRange=None): + + # argo = Argo(self._spatial, self._temporal, params, presRange) + # self.datasets["argo"] = argo + + # ---------------------------------------------------------------------- + # Methods (on all datasets) + + # error handling? what happens when one of i fails... + def search_all(self): + """ + Searches for requred dataset within platform (i.e. ICESat-2, Argo) of interest. + """ + print("\nSearching all datasets...") + + for i in self.datasets.values(): + print() + try: + # querying ICESat-2 data + if isinstance(i, Query): + print("---ICESat-2---") + msg = i.avail_granules() + print(msg) + else: # querying all other data sets + print(i) + i.search_data() + except: + dataset_name = type(i).__name__ + print("Error querying data from {0}".format(dataset_name)) + + # error handling? what happens when one of i fails... + def download_all(self, path=""): + ' ' 'Downloads requested dataset(s).' ' ' + print("\nDownloading all datasets...") + + for i in self.datasets.values(): + print() + if isinstance(i, Query): + print("---ICESat-2---") + msg = i.download_granules(path) + print(msg) + else: + i.download() + print(i) + # DEVNOTE: see colocated data branch and phyto team files for code that expands quest functionality diff --git a/icepyx/tests/test_query.py b/icepyx/tests/test_query.py index 55b25ef4a..7738c424a 100644 --- a/icepyx/tests/test_query.py +++ b/icepyx/tests/test_query.py @@ -41,6 +41,18 @@ def test_icepyx_boundingbox_query(): assert obs_tuple == exp_tuple +def test_temporal_properties_cycles_tracks(): + reg_a = ipx.Query( + "ATL06", + [-55, 68, -48, 71], + cycles=["03", "04", "05", "06", "07"], + tracks=["0849", "0902"], + ) + exp = ["No temporal parameters set"] + + assert [obs == exp for obs in (reg_a.dates, reg_a.start_time, reg_a.end_time)] + + # Tests need to add (given can't do them within docstrings/they're behind NSIDC login) # reqparams post-order # product_all_info diff --git a/icepyx/tests/test_quest.py b/icepyx/tests/test_quest.py new file mode 100644 index 000000000..043ee159e --- /dev/null +++ b/icepyx/tests/test_quest.py @@ -0,0 +1,80 @@ +import pytest +import re + +import icepyx as ipx +from icepyx.quest.quest import Quest + + +@pytest.fixture +def quest_instance(scope="module", autouse=True): + bounding_box = [-150, 30, -120, 60] + date_range = ["2022-06-07", "2022-06-14"] + my_quest = Quest(spatial_extent=bounding_box, date_range=date_range) + return my_quest + + +########## PER-DATASET ADDITION TESTS ########## + +# Paramaterize these add_dataset tests once more datasets are added +def test_add_is2(quest_instance): + # Add ATL06 as a test to QUEST + + prod = "ATL06" + quest_instance.add_icesat2(product=prod) + exp_key = "icesat2" + exp_type = ipx.Query + + obs = quest_instance.datasets + + assert type(obs) == dict + assert exp_key in obs.keys() + assert type(obs[exp_key]) == exp_type + assert quest_instance.datasets[exp_key].product == prod + + +# def test_add_argo(quest_instance): +# params = ["down_irradiance412", "temperature"] +# quest_instance.add_argo(params=params) +# exp_key = "argo" +# exp_type = ipx.quest.dataset_scripts.argo.Argo + +# obs = quest_instance.datasets + +# assert type(obs) == dict +# assert exp_key in obs.keys() +# assert type(obs[exp_key]) == exp_type +# assert quest_instance.datasets[exp_key].params == params + +# def test_add_multiple_datasets(): +# bounding_box = [-150, 30, -120, 60] +# date_range = ["2022-06-07", "2022-06-14"] +# my_quest = Quest(spatial_extent=bounding_box, date_range=date_range) +# +# # print(my_quest.spatial) +# # print(my_quest.temporal) +# +# # my_quest.add_argo(params=["down_irradiance412", "temperature"]) +# # print(my_quest.datasets["argo"].params) +# +# my_quest.add_icesat2(product="ATL06") +# # print(my_quest.datasets["icesat2"].product) +# +# print(my_quest) +# +# # my_quest.search_all() +# # +# # # this one still needs work for IS2 because of auth... +# # my_quest.download_all() + +########## ALL DATASET METHODS TESTS ########## + +# is successful execution enough here? +# each of the query functions should be tested in their respective modules +def test_search_all(quest_instance): + # Search and test all datasets + quest_instance.search_all() + + +def test_download_all(): + # this will require auth in some cases... + pass From 93867076b228bec7ca7b4832a5682662d14479bc Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Fri, 6 Oct 2023 12:32:14 -0400 Subject: [PATCH 072/124] fix incorrect merge conflict handling --- icepyx/quest/dataset_scripts/dataset.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index 9f26c83fa..e76081e08 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -4,7 +4,7 @@ warnings.filterwarnings("ignore") -class DataSet(GenQuery): +class DataSet: """ Template parent class for all QUEST supported datasets (i.e. ICESat-2, Argo BGC, Argo, MODIS, etc.). @@ -20,9 +20,7 @@ def __init__( One can also check that the "default" space and time supplied by QUEST are the right format (e.g. if the spatial extent must be a bounding box). """ - super().__init__(spatial_extent, date_range, start_time, end_time) - # self.bounding_box = boundingbox - # self.time_frame = timeframe + raise NotImplementedError # ---------------------------------------------------------------------- # Formatting API Inputs From 417929fbe8abcae8747fd5145159d368af684287 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 9 Oct 2023 12:14:08 -0400 Subject: [PATCH 073/124] uncomment argo portions of Quest --- icepyx/quest/quest.py | 29 ++++++++++++---- icepyx/tests/test_quest.py | 68 ++++++++++++++++++++------------------ 2 files changed, 57 insertions(+), 40 deletions(-) diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index c54e49b73..43ac0db0a 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -2,10 +2,9 @@ from icepyx.core.query import GenQuery, Query -# from icepyx.quest.dataset_scripts.argo import Argo +from icepyx.quest.dataset_scripts.argo import Argo -# todo: implement the subclass inheritance class Quest(GenQuery): """ QUEST - Query Unify Explore SpatioTemporal - object to query, obtain, and perform basic @@ -64,6 +63,7 @@ def __init__( """ Tells QUEST to initialize data given the user input spatiotemporal data. """ + super().__init__(spatial_extent, date_range, start_time, end_time) self.datasets = {} @@ -114,10 +114,25 @@ def add_icesat2( self.datasets["icesat2"] = query - # def add_argo(self, params=["temperature"], presRange=None): + def add_argo(self, params=["temperature"], presRange=None) -> None: + """ + Adds Argo (including Argo-BGC) to QUEST structure. + + Parameters + ---------- + For details on inputs, see the Argo dataset script documentation. + + Returns + ------- + None + + See Also + -------- + quest.dataset_scripts.argo + """ - # argo = Argo(self._spatial, self._temporal, params, presRange) - # self.datasets["argo"] = argo + argo = Argo(self._spatial, self._temporal, params, presRange) + self.datasets["argo"] = argo # ---------------------------------------------------------------------- # Methods (on all datasets) @@ -137,7 +152,7 @@ def search_all(self): print("---ICESat-2---") msg = i.avail_granules() print(msg) - else: # querying all other data sets + else: # querying all other data sets print(i) i.search_data() except: @@ -146,7 +161,7 @@ def search_all(self): # error handling? what happens when one of i fails... def download_all(self, path=""): - ' ' 'Downloads requested dataset(s).' ' ' + " " "Downloads requested dataset(s)." " " print("\nDownloading all datasets...") for i in self.datasets.values(): diff --git a/icepyx/tests/test_quest.py b/icepyx/tests/test_quest.py index 043ee159e..cd23864c1 100644 --- a/icepyx/tests/test_quest.py +++ b/icepyx/tests/test_quest.py @@ -32,39 +32,41 @@ def test_add_is2(quest_instance): assert quest_instance.datasets[exp_key].product == prod -# def test_add_argo(quest_instance): -# params = ["down_irradiance412", "temperature"] -# quest_instance.add_argo(params=params) -# exp_key = "argo" -# exp_type = ipx.quest.dataset_scripts.argo.Argo - -# obs = quest_instance.datasets - -# assert type(obs) == dict -# assert exp_key in obs.keys() -# assert type(obs[exp_key]) == exp_type -# assert quest_instance.datasets[exp_key].params == params - -# def test_add_multiple_datasets(): -# bounding_box = [-150, 30, -120, 60] -# date_range = ["2022-06-07", "2022-06-14"] -# my_quest = Quest(spatial_extent=bounding_box, date_range=date_range) -# -# # print(my_quest.spatial) -# # print(my_quest.temporal) -# -# # my_quest.add_argo(params=["down_irradiance412", "temperature"]) -# # print(my_quest.datasets["argo"].params) -# -# my_quest.add_icesat2(product="ATL06") -# # print(my_quest.datasets["icesat2"].product) -# -# print(my_quest) -# -# # my_quest.search_all() -# # -# # # this one still needs work for IS2 because of auth... -# # my_quest.download_all() +def test_add_argo(quest_instance): + params = ["down_irradiance412", "temperature"] + quest_instance.add_argo(params=params) + exp_key = "argo" + exp_type = ipx.quest.dataset_scripts.argo.Argo + + obs = quest_instance.datasets + + assert type(obs) == dict + assert exp_key in obs.keys() + assert type(obs[exp_key]) == exp_type + assert quest_instance.datasets[exp_key].params == params + + +def test_add_multiple_datasets(): + bounding_box = [-150, 30, -120, 60] + date_range = ["2022-06-07", "2022-06-14"] + my_quest = Quest(spatial_extent=bounding_box, date_range=date_range) + + # print(my_quest.spatial) + # print(my_quest.temporal) + + # my_quest.add_argo(params=["down_irradiance412", "temperature"]) + # print(my_quest.datasets["argo"].params) + + my_quest.add_icesat2(product="ATL06") + # print(my_quest.datasets["icesat2"].product) + + print(my_quest) + + # my_quest.search_all() + # + # # this one still needs work for IS2 because of auth... + # my_quest.download_all() + ########## ALL DATASET METHODS TESTS ########## From b8bf4ea8a2e5c70a3e85fbdfc12942896b05acc8 Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Tue, 10 Oct 2023 20:55:05 +0000 Subject: [PATCH 074/124] Drafted an example Jupyter notebook using both ICESat-2 and Argo through QUEST. --- argo_workflow.ipynb | 422 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 argo_workflow.ipynb diff --git a/argo_workflow.ipynb b/argo_workflow.ipynb new file mode 100644 index 000000000..efb06821e --- /dev/null +++ b/argo_workflow.ipynb @@ -0,0 +1,422 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "16806722-f5bb-4063-bd4b-60c8b0d24d2a", + "metadata": { + "user_expressions": [] + }, + "source": [ + "# QUEST Example: Finding Argo and ICESat-2 data\n", + "\n", + "In this notebook, we are going to find Argo and ICESat-2 data over a region of the Pacific Ocean. Normally, we would require multiple data portals or Python packages to accomplish this. However, thanks to the QUEST module, we can use icepyx to find both!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed25d839-4114-41db-9166-8c027368686c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Basic packages\n", + "import geopandas as gpd\n", + "import h5py\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from os import listdir\n", + "from os.path import isfile, join\n", + "import pandas as pd\n", + "from pprint import pprint\n", + "import requests\n", + "\n", + "# icepyx and QUEST\n", + "import icepyx as ipx\n", + "\n", + "import sys\n", + "sys.path.append('/home/jovyan/icesat2-snowex/')\n", + "import lidar_processing as lp" + ] + }, + { + "cell_type": "markdown", + "id": "5c35f5df-b4fb-4a36-8d6f-d20f1552767a", + "metadata": { + "user_expressions": [] + }, + "source": [ + "## Define the Quest Object\n", + "\n", + "The key to using icepyx for multiple datasets is to use the QUEST module. QUEST builds off of the general querying process originally designed for ICESat-2, but makes it applicable to other datasets.\n", + "\n", + "Just like the ICESat-2 Query object, we begin by defining our Quest object. We provide the following bounding parameters:\n", + "* `spatial_extent`: Data is constrained to the given box over the Pacific Ocean.\n", + "* `date_range`: Only grab data from April 12-26, 2022." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c5d0546d-f0b8-475d-9fd4-62ace696e316", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Spatial bounds, given as SW/NE corners\n", + "spatial_extent = [-154, 30, -143, 37]\n", + "\n", + "# Start and end dates, in YYYY-MM-DD format\n", + "date_range = ['2022-04-12', '2022-04-26']\n", + "\n", + "# Initialize the QUEST object\n", + "reg_a = ipx.Quest(spatial_extent=spatial_extent, date_range=date_range)\n", + "\n", + "print(reg_a)" + ] + }, + { + "cell_type": "markdown", + "id": "8732bf56-1d44-4182-83f7-4303a87d231a", + "metadata": { + "user_expressions": [] + }, + "source": [ + "Notice that we have defined our spatial and temporal domains, but we do not have any datasets in our QUEST object. The next section leads us through that process." + ] + }, + { + "cell_type": "markdown", + "id": "1598bbca-3dcb-4b63-aeb1-81c27d92a1a2", + "metadata": { + "user_expressions": [] + }, + "source": [ + "## Getting the data\n", + "\n", + "Let's first grab the ICESat-2 data. If we want to extract information about the water column, the ATL03 product is likely the desired choice.\n", + "* `short_name`: ATL03 data only" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "309a7b26-cfc3-46fc-a683-43e154412074", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# ICESat-2 product\n", + "short_name = 'ATL03'\n", + "\n", + "# Add ICESat-2 to QUEST datasets\n", + "reg_a.add_icesat2(product=short_name)\n", + "print(reg_a)" + ] + }, + { + "cell_type": "markdown", + "id": "ad4bbcfe-3199-4a28-8739-c930d1572538", + "metadata": { + "user_expressions": [] + }, + "source": [ + "Let's see the available files over this region." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2b4e56f-ceff-45e7-b52c-e7725dc6c812", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "pprint(reg_a.datasets['icesat2'].avail_granules(ids=True))" + ] + }, + { + "cell_type": "markdown", + "id": "7a081854-dae4-4e99-a550-02c02a71b6de", + "metadata": { + "user_expressions": [] + }, + "source": [ + "Note that many of the ICESat-2 functions shown here are the same as those used for normal icepyx queries. The user is referred to other examples for detailed explanations about other icepyx features.\n", + "\n", + "Downloading ICESat-2 data requires an Earthdata login and granule order, which is shown in the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58f509a9-e9bf-4fc3-b15b-58fcfe5c432d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Login with Earthdata credentials\n", + "reg_a.datasets['icesat2'].earthdata_login('icepyx', 'quest@icepyx.ipx')\n", + "\n", + "# Set up granule order\n", + "reg_a.datasets['icesat2'].order_granules()" + ] + }, + { + "cell_type": "markdown", + "id": "8264515a-00f1-4f57-b927-668a71294079", + "metadata": { + "user_expressions": [] + }, + "source": [ + "Now let's grab Argo data using the same constraints. This is as simple as using the below function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c857fdcc-e271-4960-86a9-02f693cc13fe", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Add argo to the desired QUEST datasets\n", + "reg_a.add_argo()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a818c5d7-d69a-4aad-90a2-bc670a54c3a7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "reg_a.download_all('/home/jovyan/icesat2-snowex/icepyx/quest-test-data/')" + ] + }, + { + "cell_type": "markdown", + "id": "6970f0ad-9364-4732-a5e6-f93cf3fc31a3", + "metadata": { + "user_expressions": [] + }, + "source": [ + "If the code worked correctly, then there should be 19 available Argo profiles, each containing `temperature` and `pressure`, compiled into a Pandas DataFrame. When BGC Argo is fully implemented to QUEST, we could add more variables to this list.\n", + "\n", + "We also have a series of files containing ICESat-2 ATL03 data. Because these data files are very large, we are only going to focus on one of these files for this example.\n", + "\n", + "Let's now load one of the ICESat-2 files and see where it passes relative to the Argo float data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "976ed530-1dc9-412f-9d2d-e51abd28c564", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Load ICESat-2 latitudes, longitudes, heights, and photon confidence (optional)\n", + "path_root = '/home/jovyan/icesat2-snowex/icepyx/quest-test-data/'\n", + "\n", + "is2_pd = pd.DataFrame()\n", + "with h5py.File(f'{path_root}processed_ATL03_20220419002753_04111506_006_02.h5', 'r') as f:\n", + " is2_pd['lat'] = f['gt2l/heights/lat_ph'][:]\n", + " is2_pd['lon'] = f['gt2l/heights/lon_ph'][:]\n", + " is2_pd['height'] = f['gt2l/heights/h_ph'][:]\n", + " is2_pd['signal_conf'] = f['gt2l/heights/signal_conf_ph'][:,1]\n", + " \n", + "# Set Argo data as its own DataFrame\n", + "argo_df = reg_a.datasets['argo'].argodata" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f9a3b8cf-f3b9-4522-841b-bf760672e37f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Convert both DataFrames into GeoDataFrames\n", + "is2_gdf = gpd.GeoDataFrame(is2_pd, \n", + " geometry=gpd.points_from_xy(is2_pd.lon, is2_pd.lat),\n", + " crs='EPSG:4326'\n", + ")\n", + "argo_gdf = gpd.GeoDataFrame(argo_df, \n", + " geometry=gpd.points_from_xy(argo_df.lon, argo_df.lat),\n", + " crs='EPSG:4326'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "86cb8463-dc14-4c1d-853e-faf7bf4300a5", + "metadata": { + "user_expressions": [] + }, + "source": [ + "To view the relative locations of ICESat-2 and Argo, the below cell uses the `explore()` function from GeoPandas. For large datasets like ICESat-2, loading the map might take a while." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ff40f7b-3a0f-4e32-8187-322a5b7cb44d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Plot ICESat-2 track (medium/high confidence photons only) on a map\n", + "m = is2_gdf[is2_gdf['signal_conf']>=3].explore(tiles='Esri.WorldImagery',\n", + " name='ICESat-2')\n", + "\n", + "# Add Argo float locations to map\n", + "argo_gdf.explore(m=m, name='Argo', marker_kwds={\"radius\": 6}, color='red')" + ] + }, + { + "cell_type": "markdown", + "id": "8b7063ec-a2f8-4509-a7ce-5b0482b48682", + "metadata": { + "user_expressions": [] + }, + "source": [ + "While we're at it, let's plot temperature and pressure profiles for each of the Argo floats in the area." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da2748b7-b174-4abb-a44a-bd73d1d36eba", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Plot vertical profile of temperature vs. pressure for all of the floats\n", + "fig, ax = plt.subplots(figsize=(12, 6))\n", + "for pid in np.unique(argo_df['profile_id']):\n", + " argo_df[argo_df['profile_id']==pid].plot(ax=ax, x='temperature', y='pressure', label=pid)\n", + "plt.gca().invert_yaxis()\n", + "plt.xlabel('Temperature [$\\degree$C]')\n", + "plt.ylabel('Pressure [hPa]')\n", + "plt.ylim([750, -10])\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "id": "08481fbb-2298-432b-bd50-df2e1ca45cf5", + "metadata": { + "user_expressions": [] + }, + "source": [ + "Lastly, let's look at some near-coincident ICESat-2 and Argo data in a multi-panel plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1269de3c-c15d-4120-8284-3b072069d5ee", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Multi-panel plot showing ICESat-2 and Argo data\n", + "\n", + "# Calculate Extent\n", + "lons = [-154, -143, -143, -154, -154]\n", + "lats = [30, 30, 37, 37, 30]\n", + "lon_margin = (max(lons) - min(lons)) * 0.1\n", + "lat_margin = (max(lats) - min(lats)) * 0.1\n", + "\n", + "# Create Plot\n", + "fig,([ax1,ax2],[ax3,ax4]) = plt.subplots(2, 2, figsize=(12, 6))\n", + "\n", + "# Plot Relative Global View\n", + "world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))\n", + "world.plot(ax=ax1, color='0.8', edgecolor='black')\n", + "argo_df.plot.scatter(ax=ax1, x='lon', y='lat', s=25.0, c='green', zorder=3, alpha=0.3)\n", + "is2_pd.plot.scatter(ax=ax1, x='lon', y='lat', s=10.0, zorder=2, alpha=0.3)\n", + "ax1.plot(lons, lats, linewidth=1.5, color='orange', zorder=2)\n", + "#df.plot(ax=ax2, x='lon', y='lat', marker='o', color='red', markersize=2.5, zorder=3)\n", + "ax1.set_xlim(-160,-100)\n", + "ax1.set_ylim(20,50)\n", + "ax1.set_aspect('equal', adjustable='box')\n", + "ax1.set_xlabel('Longitude', fontsize=18)\n", + "ax1.set_ylabel('Latitude', fontsize=18)\n", + "\n", + "# Plot Zoomed View of Ground Tracks\n", + "argo_df.plot.scatter(ax=ax2, x='lon', y='lat', s=50.0, c='green', zorder=3, alpha=0.3)\n", + "is2_pd.plot.scatter(ax=ax2, x='lon', y='lat', s=10.0, zorder=2, alpha=0.3)\n", + "ax2.plot(lons, lats, linewidth=1.5, color='orange', zorder=1)\n", + "ax2.scatter(-151.98956, 34.43885, color='orange', marker='^', s=80, zorder=4)\n", + "ax2.set_xlim(min(lons) - lon_margin, max(lons) + lon_margin)\n", + "ax2.set_ylim(min(lats) - lat_margin, max(lats) + lat_margin)\n", + "ax2.set_aspect('equal', adjustable='box')\n", + "ax2.set_xlabel('Longitude', fontsize=18)\n", + "ax2.set_ylabel('Latitude', fontsize=18)\n", + "\n", + "# Plot ICESat-2 along-track vertical profile\n", + "is2 = ax3.scatter(is2_pd[is2_pd['signal_conf']>0]['lat'], is2_pd[is2_pd['signal_conf']>0]['height']+17.55, s=0.1)\n", + "ax3.axvline(34.43885, linestyle='--', linewidth=3, color='black')\n", + "ax3.set_xlim([34.3, 34.5])\n", + "ax3.set_ylim([-15, 5])\n", + "ax3.set_xlabel('Latitude', fontsize=18)\n", + "ax3.set_ylabel('Approx. IS-2 Depth [m]', fontsize=16)\n", + "ax3.set_yticklabels(['15', '10', '5', '0', '-5'])\n", + "\n", + "# Plot vertical ocean profile of a nearby Argo float\n", + "argo_df[argo_df['profile_id']=='4903409_053'].plot(ax=ax4, x='temperature', y='pressure', linewidth=3)\n", + "ax4.set_yscale('log')\n", + "ax4.invert_yaxis()\n", + "ax4.get_legend().remove()\n", + "ax4.set_xlabel('Temperature [$\\degree$C]', fontsize=18)\n", + "ax4.set_ylabel('Argo Pressure', fontsize=16)\n", + "#ax4.set_xlim([17, 17.2])\n", + "#ax4.set_ylim([-15, 5])\n", + "\n", + "# Show Plot\n", + "plt.tight_layout()\n", + "#plt.show()\n", + "\n", + "# Save figure\n", + "#plt.savefig('/home/jovyan/icepyx/is2_argo_figure.png', dpi=500)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "base" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 5ff8c83ecfe02c1dd56b03006d645bfaf813701c Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 11 Oct 2023 11:13:34 -0400 Subject: [PATCH 075/124] combine multiple argo test files --- icepyx/tests/test_argo.py | 30 ------------------------------ icepyx/tests/test_quest_argo.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 30 deletions(-) delete mode 100644 icepyx/tests/test_argo.py diff --git a/icepyx/tests/test_argo.py b/icepyx/tests/test_argo.py deleted file mode 100644 index 647296d12..000000000 --- a/icepyx/tests/test_argo.py +++ /dev/null @@ -1,30 +0,0 @@ -# import icepyx as ipx -import pytest -import warnings -from icepyx.quest import Argo - -def test_merge_df(): - reg_a = Argo([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) - param_list = ["salinity", "temperature","down_irradiance412"] - bad_param = ["up_irradiance412"] - # param_list = ["doxy"] - - # reg_a.search_data(params=bad_param, printURL=True) - - df = reg_a.get_dataframe(params=param_list) - print(df.columns) - assert "down_irradiance412" in df.columns - assert "down_irradiance412_argoqc" in df.columns - - df = reg_a.get_dataframe(["doxy"], keep_existing=True) - assert "doxy" in df.columns - assert "doxy_argoqc" in df.columns - assert "down_irradiance412" in df.columns - assert "down_irradiance412_argoqc" in df.columns - - -def test_validate_params(): - - bad_param = ["up_irradiance412"] - - error_msg = Argo._validate_params(bad_param) \ No newline at end of file diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index f2d8a56b9..652bc2470 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -70,6 +70,22 @@ def test_download_parse_into_df(): assert len(reg_a.argodata) == 1943 +def test_merge_df(): + reg_a = Argo([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) + param_list = ["salinity", "temperature", "down_irradiance412"] + + df = reg_a.get_dataframe(params=param_list) + + assert "down_irradiance412" in df.columns + assert "down_irradiance412_argoqc" in df.columns + + df = reg_a.get_dataframe(["doxy"], keep_existing=True) + assert "doxy" in df.columns + assert "doxy_argoqc" in df.columns + assert "down_irradiance412" in df.columns + assert "down_irradiance412_argoqc" in df.columns + + """ def test_presRange_input_param(): reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) From 251bf0bd3271fb74a1b52c206eac135fa857f658 Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Wed, 18 Oct 2023 15:43:08 +0000 Subject: [PATCH 076/124] Removed redundant cells and code. --- argo_workflow.ipynb | 54 +++++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/argo_workflow.ipynb b/argo_workflow.ipynb index efb06821e..60c2e6762 100644 --- a/argo_workflow.ipynb +++ b/argo_workflow.ipynb @@ -33,11 +33,7 @@ "import requests\n", "\n", "# icepyx and QUEST\n", - "import icepyx as ipx\n", - "\n", - "import sys\n", - "sys.path.append('/home/jovyan/icesat2-snowex/')\n", - "import lidar_processing as lp" + "import icepyx as ipx" ] }, { @@ -148,23 +144,7 @@ "source": [ "Note that many of the ICESat-2 functions shown here are the same as those used for normal icepyx queries. The user is referred to other examples for detailed explanations about other icepyx features.\n", "\n", - "Downloading ICESat-2 data requires an Earthdata login and granule order, which is shown in the cell below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "58f509a9-e9bf-4fc3-b15b-58fcfe5c432d", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Login with Earthdata credentials\n", - "reg_a.datasets['icesat2'].earthdata_login('icepyx', 'quest@icepyx.ipx')\n", - "\n", - "# Set up granule order\n", - "reg_a.datasets['icesat2'].order_granules()" + "Downloading ICESat-2 data requires Earthdata login credentials, and the `download_all()` function below gives an authentication check when attempting to access the ICESat-2 files." ] }, { @@ -190,6 +170,18 @@ "reg_a.add_argo()" ] }, + { + "cell_type": "markdown", + "id": "70d36566-0d3c-4781-a199-09bb11dad975", + "metadata": { + "user_expressions": [] + }, + "source": [ + "Now we can access the data for both Argo and ICESat-2! The below function will do this for us.\n", + "\n", + "**Important**: With our current code, the Argo data will be compiled into a Pandas DataFrame, which must be manually saved by the user. The ICESat-2 data is saved as processed HDF-5 files to the directory given below." + ] + }, { "cell_type": "code", "execution_count": null, @@ -199,6 +191,7 @@ }, "outputs": [], "source": [ + "# Access Argo and ICESat-2 data simultaneously\n", "reg_a.download_all('/home/jovyan/icesat2-snowex/icepyx/quest-test-data/')" ] }, @@ -280,7 +273,7 @@ "source": [ "# Plot ICESat-2 track (medium/high confidence photons only) on a map\n", "m = is2_gdf[is2_gdf['signal_conf']>=3].explore(tiles='Esri.WorldImagery',\n", - " name='ICESat-2')\n", + " name='ICESat-2')\n", "\n", "# Add Argo float locations to map\n", "argo_gdf.explore(m=m, name='Argo', marker_kwds={\"radius\": 6}, color='red')" @@ -335,7 +328,10 @@ }, "outputs": [], "source": [ - "# Multi-panel plot showing ICESat-2 and Argo data\n", + "# Only consider ICESat-2 signal photons\n", + "is2_pd_signal = is2_pd[is2_pd['signal_conf']>0]\n", + "\n", + "## Multi-panel plot showing ICESat-2 and Argo data\n", "\n", "# Calculate Extent\n", "lons = [-154, -143, -143, -154, -154]\n", @@ -370,8 +366,8 @@ "ax2.set_xlabel('Longitude', fontsize=18)\n", "ax2.set_ylabel('Latitude', fontsize=18)\n", "\n", - "# Plot ICESat-2 along-track vertical profile\n", - "is2 = ax3.scatter(is2_pd[is2_pd['signal_conf']>0]['lat'], is2_pd[is2_pd['signal_conf']>0]['height']+17.55, s=0.1)\n", + "# Plot ICESat-2 along-track vertical profile. A dotted line notes the location of a nearby Argo float\n", + "is2 = ax3.scatter(is2_pd_signal['lat'], is2_pd_signal['height'], s=0.1)\n", "ax3.axvline(34.43885, linestyle='--', linewidth=3, color='black')\n", "ax3.set_xlim([34.3, 34.5])\n", "ax3.set_ylim([-15, 5])\n", @@ -379,19 +375,15 @@ "ax3.set_ylabel('Approx. IS-2 Depth [m]', fontsize=16)\n", "ax3.set_yticklabels(['15', '10', '5', '0', '-5'])\n", "\n", - "# Plot vertical ocean profile of a nearby Argo float\n", + "# Plot vertical ocean profile of the nearby Argo float\n", "argo_df[argo_df['profile_id']=='4903409_053'].plot(ax=ax4, x='temperature', y='pressure', linewidth=3)\n", "ax4.set_yscale('log')\n", "ax4.invert_yaxis()\n", "ax4.get_legend().remove()\n", "ax4.set_xlabel('Temperature [$\\degree$C]', fontsize=18)\n", "ax4.set_ylabel('Argo Pressure', fontsize=16)\n", - "#ax4.set_xlim([17, 17.2])\n", - "#ax4.set_ylim([-15, 5])\n", "\n", - "# Show Plot\n", "plt.tight_layout()\n", - "#plt.show()\n", "\n", "# Save figure\n", "#plt.savefig('/home/jovyan/icepyx/is2_argo_figure.png', dpi=500)" From d03f9fbbd06d8342c81f58af3c97540bbaaebaa0 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 18 Oct 2023 13:05:10 -0400 Subject: [PATCH 077/124] temporarily disable OpenAltimetry API tests (#459) * add OA API warning * comment out tests that use OA API --------- Co-authored-by: GitHub Action --- .../documentation/classes_dev_uml.svg | 300 +++++++++--------- .../documentation/classes_user_uml.svg | 206 ++++++------ icepyx/core/visualization.py | 9 +- icepyx/tests/test_visualization.py | 3 +- 4 files changed, 263 insertions(+), 255 deletions(-) diff --git a/doc/source/user_guide/documentation/classes_dev_uml.svg b/doc/source/user_guide/documentation/classes_dev_uml.svg index fd5033938..34e13b41c 100644 --- a/doc/source/user_guide/documentation/classes_dev_uml.svg +++ b/doc/source/user_guide/documentation/classes_dev_uml.svg @@ -4,11 +4,11 @@ - - + + classes_dev_uml - + icepyx.core.auth.AuthenticationError @@ -30,32 +30,38 @@ icepyx.core.auth.EarthdataAuthMixin - -EarthdataAuthMixin - -_auth : Auth, NoneType -_s3_initial_ts : NoneType, datetime -_s3login_credentials : NoneType, dict -_session : NoneType, Session -auth -s3login_credentials -session - -__init__(auth) -__str__() -earthdata_login(uid, email, s3token): None + +EarthdataAuthMixin + +_auth : Auth, NoneType +_s3_initial_ts : NoneType, datetime +_s3login_credentials : NoneType, dict +_session : NoneType +auth +s3login_credentials +session + +__init__(auth) +__str__() +earthdata_login(uid, email, s3token): None icepyx.core.query.GenQuery - -GenQuery - -_spatial -_temporal - -__init__(spatial_extent, date_range, start_time, end_time) -__str__() + +GenQuery + +_spatial +_temporal +dates +end_time +spatial +spatial_extent +start_time +temporal + +__init__(spatial_extent, date_range, start_time, end_time) +__str__() @@ -75,38 +81,32 @@ icepyx.core.query.Query - -Query - -CMRparams -_CMRparams -_about_product -_cust_options : dict -_cycles : list -_file_vars -_granules -_order_vars -_prod : NoneType, str -_readable_granule_name : list -_reqparams -_source : str -_subsetparams : NoneType -_tracks : list -_version -cycles -dataset -dates -end_time -file_vars -granules -order_vars -product -product_version -reqparams -spatial -spatial_extent -start_time -temporal + +Query + +CMRparams +_CMRparams +_about_product +_cust_options : dict +_cycles : list +_file_vars +_granules +_order_vars +_prod : NoneType, str +_readable_granule_name : list +_reqparams +_source : str +_subsetparams : NoneType +_tracks : list +_version +cycles +dataset +file_vars +granules +order_vars +product +product_version +reqparams tracks __init__(product, spatial_extent, date_range, start_time, end_time, version, cycles, tracks, files, auth) @@ -125,15 +125,15 @@ icepyx.core.granules.Granules->icepyx.core.query.Query - - + + _granules icepyx.core.granules.Granules->icepyx.core.query.Query - - + + _granules @@ -160,17 +160,17 @@ icepyx.core.exceptions.QueryError - -QueryError - - - + +QueryError + + + icepyx.core.exceptions.NsidcQueryError->icepyx.core.exceptions.QueryError - - + + @@ -195,122 +195,122 @@ icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - + + _CMRparams icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - + + _reqparams icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - + + _subsetparams icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - + + _subsetparams icepyx.core.query.Query->icepyx.core.auth.EarthdataAuthMixin - - + + icepyx.core.query.Query->icepyx.core.query.GenQuery - - + + icepyx.core.read.Read - -Read - -_filelist : list, NoneType -_out_obj : Dataset -_pattern : str -_prod : str -_read_vars -_source_type : str -data_source -vars - -__init__(data_source, product, filename_pattern, catalog, out_obj_type) -_add_vars_to_ds(is2ds, ds, grp_path, wanted_groups_tiered, wanted_dict) -_build_dataset_template(file) -_build_single_file_dataset(file, groups_list) -_check_source_for_pattern(source, filename_pattern) -_combine_nested_vars(is2ds, ds, grp_path, wanted_dict) -_read_single_grp(file, grp_path) -load() + +Read + +_filelist : NoneType, list +_out_obj : Dataset +_pattern : str +_prod : str +_read_vars +_source_type : str +data_source +vars + +__init__(data_source, product, filename_pattern, catalog, out_obj_type) +_add_vars_to_ds(is2ds, ds, grp_path, wanted_groups_tiered, wanted_dict) +_build_dataset_template(file) +_build_single_file_dataset(file, groups_list) +_check_source_for_pattern(source, filename_pattern) +_combine_nested_vars(is2ds, ds, grp_path, wanted_dict) +_read_single_grp(file, grp_path) +load() icepyx.core.spatial.Spatial - -Spatial - -_ext_type : str -_gdf_spat : GeoDataFrame, DataFrame -_geom_file : NoneType -_spatial_ext -_xdateln -extent -extent_as_gdf -extent_file -extent_type - -__init__(spatial_extent) -__str__() -fmt_for_CMR() -fmt_for_EGI() + +Spatial + +_ext_type : str +_gdf_spat : GeoDataFrame +_geom_file : NoneType +_spatial_ext +_xdateln +extent +extent_as_gdf +extent_file +extent_type + +__init__(spatial_extent) +__str__() +fmt_for_CMR() +fmt_for_EGI() icepyx.core.spatial.Spatial->icepyx.core.query.GenQuery - - -_spatial + + +_spatial icepyx.core.spatial.Spatial->icepyx.core.query.GenQuery - - -_spatial + + +_spatial icepyx.core.temporal.Temporal - -Temporal - -_end : datetime -_start : datetime -end -start - -__init__(date_range, start_time, end_time) -__str__() + +Temporal + +_end : datetime +_start : datetime +end +start + +__init__(date_range, start_time, end_time) +__str__() icepyx.core.temporal.Temporal->icepyx.core.query.GenQuery - - -_temporal + + +_temporal @@ -339,36 +339,36 @@ icepyx.core.variables.Variables->icepyx.core.auth.EarthdataAuthMixin - - + + icepyx.core.variables.Variables->icepyx.core.query.Query - - + + _order_vars icepyx.core.variables.Variables->icepyx.core.query.Query - - -_order_vars + + +_order_vars icepyx.core.variables.Variables->icepyx.core.query.Query - - -_file_vars + + +_file_vars icepyx.core.variables.Variables->icepyx.core.read.Read - - -_read_vars + + +_read_vars diff --git a/doc/source/user_guide/documentation/classes_user_uml.svg b/doc/source/user_guide/documentation/classes_user_uml.svg index 1c9184379..640f76815 100644 --- a/doc/source/user_guide/documentation/classes_user_uml.svg +++ b/doc/source/user_guide/documentation/classes_user_uml.svg @@ -4,11 +4,11 @@ - - + + classes_user_uml - + icepyx.core.auth.AuthenticationError @@ -30,23 +30,29 @@ icepyx.core.auth.EarthdataAuthMixin - -EarthdataAuthMixin - -auth -s3login_credentials -session - -earthdata_login(uid, email, s3token): None + +EarthdataAuthMixin + +auth +s3login_credentials +session + +earthdata_login(uid, email, s3token): None icepyx.core.query.GenQuery - -GenQuery - - - + +GenQuery + +dates +end_time +spatial +spatial_extent +start_time +temporal + + @@ -64,24 +70,18 @@ icepyx.core.query.Query - -Query - -CMRparams -cycles -dataset -dates -end_time -file_vars -granules -order_vars -product -product_version -reqparams -spatial -spatial_extent -start_time -temporal + +Query + +CMRparams +cycles +dataset +file_vars +granules +order_vars +product +product_version +reqparams tracks avail_granules(ids, cycles, tracks, cloud) @@ -98,15 +98,15 @@ icepyx.core.granules.Granules->icepyx.core.query.Query - - + + _granules icepyx.core.granules.Granules->icepyx.core.query.Query - - + + _granules @@ -132,17 +132,17 @@ icepyx.core.exceptions.QueryError - -QueryError - - - + +QueryError + + + icepyx.core.exceptions.NsidcQueryError->icepyx.core.exceptions.QueryError - - + + @@ -161,99 +161,99 @@ icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - + + _CMRparams icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - + + _reqparams icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - + + _subsetparams icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - + + _subsetparams icepyx.core.query.Query->icepyx.core.auth.EarthdataAuthMixin - - + + icepyx.core.query.Query->icepyx.core.query.GenQuery - - + + icepyx.core.read.Read - -Read - -data_source -vars - -load() + +Read + +data_source +vars + +load() icepyx.core.spatial.Spatial - -Spatial - -extent -extent_as_gdf -extent_file -extent_type - -fmt_for_CMR() -fmt_for_EGI() + +Spatial + +extent +extent_as_gdf +extent_file +extent_type + +fmt_for_CMR() +fmt_for_EGI() icepyx.core.spatial.Spatial->icepyx.core.query.GenQuery - - -_spatial + + +_spatial icepyx.core.spatial.Spatial->icepyx.core.query.GenQuery - - -_spatial + + +_spatial icepyx.core.temporal.Temporal - -Temporal - -end -start - - + +Temporal + +end +start + + icepyx.core.temporal.Temporal->icepyx.core.query.GenQuery - - -_temporal + + +_temporal @@ -273,35 +273,35 @@ icepyx.core.variables.Variables->icepyx.core.auth.EarthdataAuthMixin - - + + icepyx.core.variables.Variables->icepyx.core.query.Query - - + + _order_vars icepyx.core.variables.Variables->icepyx.core.query.Query - - -_order_vars + + +_order_vars icepyx.core.variables.Variables->icepyx.core.query.Query - - -_file_vars + + +_file_vars icepyx.core.variables.Variables->icepyx.core.read.Read - - + + _read_vars diff --git a/icepyx/core/visualization.py b/icepyx/core/visualization.py index c6bef2333..a2b8fe5dc 100644 --- a/icepyx/core/visualization.py +++ b/icepyx/core/visualization.py @@ -4,6 +4,7 @@ import concurrent.futures import datetime import re +import warnings import backoff import dask.array as da @@ -332,7 +333,13 @@ def request_OA_data(self, paras) -> da.array: A dask array containing the ICESat-2 elevation data. """ - base_url = "https://openaltimetry.org/data/api/icesat2/level3a" + warnings.warn( + "NOTICE: visualizations requiring the OpenAltimetry API are currently (October 2023) ", + "unavailable while hosting of OpenAltimetry transitions from UCSD to NSIDC.", + "A ticket has been issued to restore programmatic API access.", + ) + + base_url = "http://openaltimetry.earthdatacloud.nasa.gov/data/api/icesat2" trackId, Date, cycle, bbox, product = paras # Generate API diff --git a/icepyx/tests/test_visualization.py b/icepyx/tests/test_visualization.py index 8056a453f..0a1f2fa43 100644 --- a/icepyx/tests/test_visualization.py +++ b/icepyx/tests/test_visualization.py @@ -70,7 +70,7 @@ def test_gran_paras(filename, expect): # 2023-01-27: for the commented test below, r (in visualization line 444) is returning None even though I can see OA data there via a browser - +""" @pytest.mark.parametrize( "product, date_range, bbox, expect", [ @@ -112,3 +112,4 @@ def test_visualization_orbits(product, bbox, cycles, tracks, expect): data_size = region_viz.parallel_request_OA().size assert data_size == expect +""" From ee8b79fda74b4c6d59683e3124092bfa5afa2e6f Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 18 Oct 2023 13:23:05 -0400 Subject: [PATCH 078/124] fix spot number calculation (#458) --------- Co-authored-by: GitHub Action --- icepyx/core/is2ref.py | 23 ++++++++++++++--------- icepyx/core/visualization.py | 6 +++--- icepyx/tests/test_is2ref.py | 16 ++++++++-------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/icepyx/core/is2ref.py b/icepyx/core/is2ref.py index 52cf0e3a1..a3a0311bb 100644 --- a/icepyx/core/is2ref.py +++ b/icepyx/core/is2ref.py @@ -265,8 +265,11 @@ def _default_varlists(product): return common_list -# dev goal: check and test this function def gt2spot(gt, sc_orient): + warnings.warn( + "icepyx versions 0.8.0 and earlier used an incorrect spot number calculation." + "As a result, computations depending on spot number may be incorrect and should be redone." + ) assert gt in [ "gt1l", @@ -280,12 +283,13 @@ def gt2spot(gt, sc_orient): gr_num = np.uint8(gt[2]) gr_lr = gt[3] + # spacecraft oriented forward if sc_orient == 1: if gr_num == 1: if gr_lr == "l": - spot = 2 + spot = 6 elif gr_lr == "r": - spot = 1 + spot = 5 elif gr_num == 2: if gr_lr == "l": spot = 4 @@ -293,16 +297,17 @@ def gt2spot(gt, sc_orient): spot = 3 elif gr_num == 3: if gr_lr == "l": - spot = 6 + spot = 2 elif gr_lr == "r": - spot = 5 + spot = 1 + # spacecraft oriented backward elif sc_orient == 0: if gr_num == 1: if gr_lr == "l": - spot = 5 + spot = 1 elif gr_lr == "r": - spot = 6 + spot = 2 elif gr_num == 2: if gr_lr == "l": spot = 3 @@ -310,9 +315,9 @@ def gt2spot(gt, sc_orient): spot = 4 elif gr_num == 3: if gr_lr == "l": - spot = 1 + spot = 5 elif gr_lr == "r": - spot = 2 + spot = 6 if "spot" not in locals(): raise ValueError("Could not compute the spot number.") diff --git a/icepyx/core/visualization.py b/icepyx/core/visualization.py index a2b8fe5dc..32c81e3e7 100644 --- a/icepyx/core/visualization.py +++ b/icepyx/core/visualization.py @@ -334,9 +334,9 @@ def request_OA_data(self, paras) -> da.array: """ warnings.warn( - "NOTICE: visualizations requiring the OpenAltimetry API are currently (October 2023) ", - "unavailable while hosting of OpenAltimetry transitions from UCSD to NSIDC.", - "A ticket has been issued to restore programmatic API access.", + "NOTICE: visualizations requiring the OpenAltimetry API are currently (October 2023) " + "unavailable while hosting of OpenAltimetry transitions from UCSD to NSIDC." + "A ticket has been issued to restore programmatic API access." ) base_url = "http://openaltimetry.earthdatacloud.nasa.gov/data/api/icesat2" diff --git a/icepyx/tests/test_is2ref.py b/icepyx/tests/test_is2ref.py index 8d50568fe..b22709c98 100644 --- a/icepyx/tests/test_is2ref.py +++ b/icepyx/tests/test_is2ref.py @@ -556,12 +556,12 @@ def test_unsupported_default_varlist(): def test_gt2spot_sc_orient_1(): # gt1l obs = is2ref.gt2spot("gt1l", 1) - expected = 2 + expected = 6 assert obs == expected # gt1r obs = is2ref.gt2spot("gt1r", 1) - expected = 1 + expected = 5 assert obs == expected # gt2l @@ -576,24 +576,24 @@ def test_gt2spot_sc_orient_1(): # gt3l obs = is2ref.gt2spot("gt3l", 1) - expected = 6 + expected = 2 assert obs == expected # gt3r obs = is2ref.gt2spot("gt3r", 1) - expected = 5 + expected = 1 assert obs == expected def test_gt2spot_sc_orient_0(): # gt1l obs = is2ref.gt2spot("gt1l", 0) - expected = 5 + expected = 1 assert obs == expected # gt1r obs = is2ref.gt2spot("gt1r", 0) - expected = 6 + expected = 2 assert obs == expected # gt2l @@ -608,10 +608,10 @@ def test_gt2spot_sc_orient_0(): # gt3l obs = is2ref.gt2spot("gt3l", 0) - expected = 1 + expected = 5 assert obs == expected # gt3r obs = is2ref.gt2spot("gt3r", 0) - expected = 2 + expected = 6 assert obs == expected From a1a723dac6a5c1c4b531c0079440705fb7d27052 Mon Sep 17 00:00:00 2001 From: Whyjay Zheng Date: Thu, 19 Oct 2023 01:39:02 +0800 Subject: [PATCH 079/124] Fix a broken link in IS2_data_access.ipynb (#456) --- doc/source/example_notebooks/IS2_data_access.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/example_notebooks/IS2_data_access.ipynb b/doc/source/example_notebooks/IS2_data_access.ipynb index d9d50cdc0..0b4a12244 100644 --- a/doc/source/example_notebooks/IS2_data_access.ipynb +++ b/doc/source/example_notebooks/IS2_data_access.ipynb @@ -79,7 +79,7 @@ "\n", "There are three required inputs, depending on how you want to search for data. Two are required in all cases:\n", "- `short_name` = the data product of interest, known as its \"short name\".\n", - "See https://nsidc.org/data/icesat-2/data-sets for a list of the available data products.\n", + "See https://nsidc.org/data/icesat-2/products for a list of the available data products.\n", "- `spatial extent` = a region of interest to search within. This can be entered as a bounding box, polygon vertex coordinate pairs, or a polygon geospatial file (currently shp, kml, and gpkg are supported).\n", " - bounding box: Given in decimal degrees for the lower left longitude, lower left latitude, upper right longitude, and upper right latitude\n", " - polygon vertices: Given as longitude, latitude coordinate pairs of decimal degrees with the last entry a repeat of the first.\n", From d86cc9e23e61b6328d603e351656396e8fd33697 Mon Sep 17 00:00:00 2001 From: Rachel Wegener <35503632+rwegener2@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:02:15 -0400 Subject: [PATCH 080/124] update Read input arguments (#444) * add filelist and product properties to Read object * deprecate filename_pattern and product class Read inputs * transition to data_source input as a string (including glob string) or list * update tutorial with changes and user guidance for using glob --------- Co-authored-by: Jessica Scheick --- .../example_notebooks/IS2_data_read-in.ipynb | 182 +++++++++++----- .../documentation/classes_dev_uml.svg | 122 +++++------ .../documentation/classes_user_uml.svg | 21 +- doc/source/user_guide/documentation/read.rst | 2 + icepyx/core/is2ref.py | 5 +- icepyx/core/read.py | 206 +++++++++++++----- icepyx/tests/test_is2ref.py | 4 +- 7 files changed, 356 insertions(+), 186 deletions(-) diff --git a/doc/source/example_notebooks/IS2_data_read-in.ipynb b/doc/source/example_notebooks/IS2_data_read-in.ipynb index 115c63044..9bbac368b 100644 --- a/doc/source/example_notebooks/IS2_data_read-in.ipynb +++ b/doc/source/example_notebooks/IS2_data_read-in.ipynb @@ -63,9 +63,8 @@ "metadata": {}, "outputs": [], "source": [ - "path_root = '/full/path/to/your/data/'\n", - "pattern = \"processed_ATL{product:2}_{datetime:%Y%m%d%H%M%S}_{rgt:4}{cycle:2}{orbitsegment:2}_{version:3}_{revision:2}.h5\"\n", - "reader = ipx.Read(path_root, \"ATL06\", pattern) # or ipx.Read(filepath, \"ATLXX\") if your filenames match the default pattern" + "path_root = '/full/path/to/your/ATL06_data/'\n", + "reader = ipx.Read(path_root)" ] }, { @@ -111,10 +110,9 @@ "\n", "Reading in ICESat-2 data with icepyx happens in a few simple steps:\n", "1. Let icepyx know where to find your data (this might be local files or urls to data in cloud storage)\n", - "2. Tell icepyx how to interpret the filename format\n", - "3. Create an icepyx `Read` object\n", - "4. Make a list of the variables you want to read in (does not apply for gridded products)\n", - "5. Load your data into memory (or read it in lazily, if you're using Dask)\n", + "2. Create an icepyx `Read` object\n", + "3. Make a list of the variables you want to read in (does not apply for gridded products)\n", + "4. Load your data into memory (or read it in lazily, if you're using Dask)\n", "\n", "We go through each of these steps in more detail in this notebook." ] @@ -168,21 +166,18 @@ { "cell_type": "markdown", "id": "e8da42c1", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "### Step 1: Set data source path\n", "\n", "Provide a full path to the data to be read in (i.e. opened).\n", "Currently accepted inputs are:\n", - "* a directory\n", - "* a single file\n", - "\n", - "All files to be read in *must* have a consistent filename pattern.\n", - "If a directory is supplied as the data source, all files in any subdirectories that match the filename pattern will be included.\n", - "\n", - "S3 bucket data access is currently under development, and requires you are registered with NSIDC as a beta tester for cloud-based ICESat-2 data.\n", - "icepyx is working to ensure a smooth transition to working with remote files.\n", - "We'd love your help exploring and testing these features as they become available!" + "* a string path to directory - all files from the directory will be opened\n", + "* a string path to single file - one file will be opened\n", + "* a list of filepaths - all files in the list will be opened\n", + "* a glob string (see [glob](https://docs.python.org/3/library/glob.html)) - any files matching the glob pattern will be opened" ] }, { @@ -208,86 +203,147 @@ { "cell_type": "code", "execution_count": null, - "id": "e683ebf7", + "id": "fac636c2-e0eb-4e08-adaa-8f47623e46a1", "metadata": {}, "outputs": [], "source": [ - "# urlpath = 's3://nsidc-cumulus-prod-protected/ATLAS/ATL03/004/2019/11/30/ATL03_20191130221008_09930503_004_01.h5'" + "# list_of_files = ['/my/data/ATL06/processed_ATL06_20190226005526_09100205_006_02.h5', \n", + "# '/my/other/data/ATL06/processed_ATL06_20191202102922_10160505_006_01.h5']" ] }, { "cell_type": "markdown", - "id": "92743496", + "id": "ba3ebeb0-3091-4712-b0f7-559ddb95ca5a", "metadata": { "user_expressions": [] }, "source": [ - "### Step 2: Create a filename pattern for your data files\n", + "#### Glob Strings\n", + "\n", + "[glob](https://docs.python.org/3/library/glob.html) is a Python library which allows users to list files in their file systems whose paths match a given pattern. Icepyx uses the glob library to give users greater flexibility over their input file lists.\n", + "\n", + "glob works using `*` and `?` as wildcard characters, where `*` matches any number of characters and `?` matches a single character. For example:\n", "\n", - "Files provided by NSIDC typically match the format `\"ATL{product:2}_{datetime:%Y%m%d%H%M%S}_{rgt:4}{cycle:2}{orbitsegment:2}_{version:3}_{revision:2}.h5\"` where the parameters in curly brackets indicate a parameter name (left of the colon) and character length or format (right of the colon).\n", - "Some of this information is used during data opening to help correctly read and label the data within the data structure, particularly when multiple files are opened simultaneously.\n", + "* `/this/path/*.h5`: refers to all `.h5` files in the `/this/path` folder (Example matches: \"/this/path/processed_ATL03_20191130221008_09930503_006_01.h5\" or \"/this/path/myfavoriteicsat-2file.h5\")\n", + "* `/this/path/*ATL07*.h5`: refers to all `.h5` files in the `/this/path` folder that have ATL07 in the filename. (Example matches: \"/this/path/ATL07-02_20221012220720_03391701_005_01.h5\" or \"/this/path/processed_ATL07.h5\")\n", + "* `/this/path/ATL??/*.h5`: refers to all `.h5` files that are in a subfolder of `/this/path` and a subdirectory of `ATL` followed by any 2 characters (Example matches: \"/this/path/ATL03/processed_ATL03_20191130221008_09930503_006_01.h5\", \"/this/path/ATL06/myfile.h5\")\n", "\n", - "By default, icepyx will assume your filenames follow the default format.\n", - "However, you can easily read in other ICESat-2 data files by supplying your own filename pattern.\n", - "For instance, `pattern=\"ATL{product:2}-{datetime:%Y%m%d%H%M%S}-Sample.h5\"`. A few example patterns are provided below." + "See the glob documentation or other online explainer tutorials for more in depth explanation, or advanced glob paths such as character classes and ranges." ] }, { - "cell_type": "code", - "execution_count": null, - "id": "7318abd0", - "metadata": {}, - "outputs": [], + "cell_type": "markdown", + "id": "20286c76-5632-4420-b2c9-a5a6b1952672", + "metadata": { + "user_expressions": [] + }, + "source": [ + "#### Recursive Directory Search" + ] + }, + { + "cell_type": "markdown", + "id": "632bd1ce-2397-4707-a63f-9d5d2fc02fbc", + "metadata": { + "user_expressions": [] + }, + "source": [ + "glob will not by default search all of the subdirectories for matching filepaths, but it has the ability to do so.\n", + "\n", + "If you would like to search recursively, you can achieve this by either:\n", + "1. passing the `recursive` argument into `glob_kwargs` and including `\\**\\` in your filepath\n", + "2. using glob directly to create a list of filepaths\n", + "\n", + "Each of these two methods are shown below." + ] + }, + { + "cell_type": "markdown", + "id": "da0cacd8-9ddc-4c31-86b6-167d850b989e", + "metadata": { + "user_expressions": [] + }, "source": [ - "# pattern = 'ATL06-{datetime:%Y%m%d%H%M%S}-Sample.h5'\n", - "# pattern = 'ATL{product:2}-{datetime:%Y%m%d%H%M%S}-Sample.h5'" + "Method 1: passing the `recursive` argument into `glob_kwargs`" ] }, { "cell_type": "code", "execution_count": null, - "id": "f43e8664", + "id": "e276b876-9ec7-4991-8520-05c97824b896", "metadata": {}, "outputs": [], "source": [ - "# pattern = \"ATL{product:2}_{datetime:%Y%m%d%H%M%S}_{rgt:4}{cycle:2}{orbitsegment:2}_{version:3}_{revision:2}.h5\"" + "ipx.Read('/path/to/**/folder', glob_kwargs={'recursive': True})" + ] + }, + { + "cell_type": "markdown", + "id": "f5a1e85e-fc4a-405f-9710-0cb61b827f2c", + "metadata": { + "user_expressions": [] + }, + "source": [ + "You can use `glob_kwargs` for any additional argument to Python's builtin `glob.glob` that you would like to pass in via icepyx." + ] + }, + { + "cell_type": "markdown", + "id": "76de9539-710c-49f6-9e9e-238849382c33", + "metadata": { + "user_expressions": [] + }, + "source": [ + "Method 2: using glob directly to create a list of filepaths" ] }, { "cell_type": "code", "execution_count": null, - "id": "992a77fb", + "id": "be79b0dd-efcf-4d50-bdb0-8e3ae8e8e38c", "metadata": {}, "outputs": [], "source": [ - "# grid_pattern = \"ATL{product:2}_GL_0311_{res:3}m_{version:3}_{revision:2}.nc\"" + "import glob" ] }, { "cell_type": "code", "execution_count": null, - "id": "6aec1a70", - "metadata": {}, + "id": "5d088571-496d-479a-9fb7-833ed7e98676", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "pattern = \"processed_ATL{product:2}_{datetime:%Y%m%d%H%M%S}_{rgt:4}{cycle:2}{orbitsegment:2}_{version:3}_{revision:2}.h5\"" + "list_of_files = glob.glob('/path/to/**/folder', recursive=True)\n", + "ipx.Read(list_of_files)" ] }, { "cell_type": "markdown", - "id": "4275b04c", + "id": "08df2874-7c54-4670-8f37-9135ea296ff5", "metadata": { "user_expressions": [] }, "source": [ - "### Step 3: Create an icepyx read object\n", + "```{admonition} Read Module Update\n", + "Previously, icepyx required two additional conditions: 1) a `product` argument and 2) that your files either matched the default `filename_pattern` or that the user provided their own `filename_pattern`. These two requirements have been removed. `product` is now read directly from the file metadata (the root group's `short_name` attribute). Flexibility to specify multiple files via the `filename_pattern` has been replaced with the [glob string](https://docs.python.org/3/library/glob.html) feature, and by allowing a list of filepaths as an argument.\n", "\n", - "The `Read` object has two required inputs:\n", - "- `path` = a string with the full file path or full directory path to your hdf5 (.h5) format files.\n", - "- `product` = the data product you're working with, also known as the \"short name\".\n", + "The `product` and `filename_pattern` arguments have been maintained for backwards compatibility, but will be fully removed in icepyx version 1.0.0.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "4275b04c", + "metadata": { + "user_expressions": [] + }, + "source": [ + "### Step 2: Create an icepyx read object\n", "\n", - "The `Read` object also accepts the optional keyword input:\n", - "- `pattern` = a formatted string indicating the filename pattern required for Intake's path_as_pattern argument." + "Using the `data_source` described in Step 1, we can create our Read object." ] }, { @@ -299,7 +355,17 @@ }, "outputs": [], "source": [ - "reader = ipx.Read(data_source=path_root, product=\"ATL06\", filename_pattern=pattern) # or ipx.Read(filepath, \"ATLXX\") if your filenames match the default pattern" + "reader = ipx.Read(data_source=path_root)" + ] + }, + { + "cell_type": "markdown", + "id": "7b2acfdb-75eb-4c64-b583-2ab19326aaee", + "metadata": { + "user_expressions": [] + }, + "source": [ + "The Read object now contains the list of matching files that will eventually be loaded into Python. You can inspect its properties, such as the files that were located or the identified product, directly on the Read object." ] }, { @@ -309,7 +375,17 @@ "metadata": {}, "outputs": [], "source": [ - "reader._filelist" + "reader.filelist" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7455ee3f-f9ab-486e-b4c7-2fa2314d4084", + "metadata": {}, + "outputs": [], + "source": [ + "reader.product" ] }, { @@ -319,7 +395,7 @@ "user_expressions": [] }, "source": [ - "### Step 4: Specify variables to be read in\n", + "### Step 3: Specify variables to be read in\n", "\n", "To load your data into memory or prepare it for analysis, icepyx needs to know which variables you'd like to read in.\n", "If you've used icepyx to download data from NSIDC with variable subsetting (which is the default), then you may already be familiar with the icepyx `Variables` module and how to create and modify lists of variables.\n", @@ -426,7 +502,7 @@ "user_expressions": [] }, "source": [ - "### Step 5: Loading your data\n", + "### Step 4: Loading your data\n", "\n", "Now that you've set up all the options, you're ready to read your ICESat-2 data into memory!" ] @@ -541,9 +617,9 @@ ], "metadata": { "kernelspec": { - "display_name": "general", + "display_name": "icepyx-dev", "language": "python", - "name": "general" + "name": "icepyx-dev" }, "language_info": { "codemirror_mode": { diff --git a/doc/source/user_guide/documentation/classes_dev_uml.svg b/doc/source/user_guide/documentation/classes_dev_uml.svg index 34e13b41c..0cd08c9e9 100644 --- a/doc/source/user_guide/documentation/classes_dev_uml.svg +++ b/doc/source/user_guide/documentation/classes_dev_uml.svg @@ -4,11 +4,11 @@ - + classes_dev_uml - + icepyx.core.auth.AuthenticationError @@ -139,38 +139,38 @@ icepyx.core.icesat2data.Icesat2Data - -Icesat2Data - - -__init__() + +Icesat2Data + + +__init__() icepyx.core.exceptions.NsidcQueryError - -NsidcQueryError - -errmsg -msgtxt : str - -__init__(errmsg, msgtxt) -__str__() + +NsidcQueryError + +errmsg +msgtxt : str + +__init__(errmsg, msgtxt) +__str__() icepyx.core.exceptions.QueryError - -QueryError - - - + +QueryError + + + icepyx.core.exceptions.NsidcQueryError->icepyx.core.exceptions.QueryError - - + + @@ -235,24 +235,24 @@ icepyx.core.read.Read - -Read - -_filelist : NoneType, list -_out_obj : Dataset -_pattern : str -_prod : str -_read_vars -_source_type : str -data_source -vars - -__init__(data_source, product, filename_pattern, catalog, out_obj_type) -_add_vars_to_ds(is2ds, ds, grp_path, wanted_groups_tiered, wanted_dict) -_build_dataset_template(file) -_build_single_file_dataset(file, groups_list) -_check_source_for_pattern(source, filename_pattern) -_combine_nested_vars(is2ds, ds, grp_path, wanted_dict) + +Read + +_filelist : NoneType, list +_out_obj : Dataset +_product : NoneType, str +_read_vars +filelist +product +vars + +__init__(data_source, product, filename_pattern, catalog, glob_kwargs, out_obj_type) +_add_vars_to_ds(is2ds, ds, grp_path, wanted_groups_tiered, wanted_dict) +_build_dataset_template(file) +_build_single_file_dataset(file, groups_list) +_check_source_for_pattern(source, filename_pattern) +_combine_nested_vars(is2ds, ds, grp_path, wanted_dict) +_extract_product(filepath) _read_single_grp(file, grp_path) load() @@ -366,30 +366,30 @@ icepyx.core.variables.Variables->icepyx.core.read.Read - - -_read_vars + + +_read_vars icepyx.core.visualization.Visualize - -Visualize - -bbox : list -cycles : NoneType -date_range : NoneType -product : NoneType, str -tracks : NoneType - -__init__(query_obj, product, spatial_extent, date_range, cycles, tracks) -generate_OA_parameters(): list -grid_bbox(binsize): list -make_request(base_url, payload) -parallel_request_OA(): da.array -query_icesat2_filelist(): tuple -request_OA_data(paras): da.array -viz_elevation(): (hv.DynamicMap, hv.Layout) + +Visualize + +bbox : list +cycles : NoneType +date_range : NoneType +product : NoneType, str +tracks : NoneType + +__init__(query_obj, product, spatial_extent, date_range, cycles, tracks) +generate_OA_parameters(): list +grid_bbox(binsize): list +make_request(base_url, payload) +parallel_request_OA(): da.array +query_icesat2_filelist(): tuple +request_OA_data(paras): da.array +viz_elevation(): (hv.DynamicMap, hv.Layout) diff --git a/doc/source/user_guide/documentation/classes_user_uml.svg b/doc/source/user_guide/documentation/classes_user_uml.svg index 640f76815..a9c116469 100644 --- a/doc/source/user_guide/documentation/classes_user_uml.svg +++ b/doc/source/user_guide/documentation/classes_user_uml.svg @@ -201,13 +201,14 @@ icepyx.core.read.Read - -Read - -data_source -vars - -load() + +Read + +filelist +product +vars + +load() @@ -300,9 +301,9 @@ icepyx.core.variables.Variables->icepyx.core.read.Read - - -_read_vars + + +_read_vars diff --git a/doc/source/user_guide/documentation/read.rst b/doc/source/user_guide/documentation/read.rst index a5beedf4e..68da03b1d 100644 --- a/doc/source/user_guide/documentation/read.rst +++ b/doc/source/user_guide/documentation/read.rst @@ -19,6 +19,8 @@ Attributes .. autosummary:: :toctree: ../../_icepyx/ + Read.filelist + Read.product Read.vars diff --git a/icepyx/core/is2ref.py b/icepyx/core/is2ref.py index a3a0311bb..5faaef110 100644 --- a/icepyx/core/is2ref.py +++ b/icepyx/core/is2ref.py @@ -15,6 +15,7 @@ def _validate_product(product): """ Confirm a valid ICESat-2 product was specified """ + error_msg = "A valid product string was not provided. Check user input, if given, or file metadata." if isinstance(product, str): product = str.upper(product) assert product in [ @@ -40,9 +41,9 @@ def _validate_product(product): "ATL20", "ATL21", "ATL23", - ], "Please enter a valid product" + ], error_msg else: - raise TypeError("Please enter a product string") + raise TypeError(error_msg) return product diff --git a/icepyx/core/read.py b/icepyx/core/read.py index a7ee15db7..a85ee659b 100644 --- a/icepyx/core/read.py +++ b/icepyx/core/read.py @@ -1,7 +1,9 @@ import fnmatch +import glob import os import warnings +import h5py import numpy as np import xarray as xr @@ -10,8 +12,6 @@ from icepyx.core.variables import Variables as Variables from icepyx.core.variables import list_of_dict_vals -# from icepyx.core.query import Query - def _make_np_datetime(df, keyword): """ @@ -266,24 +266,28 @@ class Read: Parameters ---------- - data_source : string - A string with a full file path or full directory path to ICESat-2 hdf5 (.h5) format files. - Files within a directory must have a consistent filename pattern that includes the "ATL??" data product name. - Files must all be within a single directory. + data_source : string, List + A string or list which specifies the files to be read. The string can be either: 1) the path of a single file 2) the path to a directory or 3) a [glob string](https://docs.python.org/3/library/glob.html). + The List must be a list of strings, each of which is the path of a single file. product : string ICESat-2 data product ID, also known as "short name" (e.g. ATL03). Available data products can be found at: https://nsidc.org/data/icesat-2/data-sets + **Deprecation warning:** This argument is no longer required and will be deprecated in version 1.0.0. The dataset product is read from the file metadata. - filename_pattern : string, default 'ATL{product:2}_{datetime:%Y%m%d%H%M%S}_{rgt:4}{cycle:2}{orbitsegment:2}_{version:3}_{revision:2}.h5' - String that shows the filename pattern as required for Intake's path_as_pattern argument. + filename_pattern : string, default None + String that shows the filename pattern as previously required for Intake's path_as_pattern argument. The default describes files downloaded directly from NSIDC (subsetted and non-subsetted) for most products (e.g. ATL06). The ATL11 filename pattern from NSIDC is: 'ATL{product:2}_{rgt:4}{orbitsegment:2}_{cycles:4}_{version:3}_{revision:2}.h5'. - + **Deprecation warning:** This argument is no longer required and will be deprecated in version 1.0.0. + catalog : string, default None Full path to an Intake catalog for reading in data. If you still need to create a catalog, leave as default. - **Deprecation warning:** This argument has been depreciated. Please use the data_source argument to pass in valid data. + **Deprecation warning:** This argument has been deprecated. Please use the data_source argument to pass in valid data. + + glob_kwargs : dict, default {} + Additional arguments to be passed into the [glob.glob()](https://docs.python.org/3/library/glob.html#glob.glob)function out_obj_type : object, default xarray.Dataset The desired format for the data to be read in. @@ -296,6 +300,21 @@ class Read: Examples -------- + Reading a single file + >>> ipx.Read('/path/to/data/processed_ATL06_20190226005526_09100205_006_02.h5') # doctest: +SKIP + + Reading all files in a directory + >>> ipx.Read('/path/to/data/') # doctest: +SKIP + + Reading files that match a particular pattern (here, all .h5 files that start with `processed_ATL06_`). + >>> ipx.Read('/path/to/data/processed_ATL06_*.h5') # doctest: +SKIP + + Reading a specific list of files + >>> list_of_files = [ + ... '/path/to/data/processed_ATL06_20190226005526_09100205_006_02.h5', + ... '/path/to/more/data/processed_ATL06_20191202102922_10160505_006_01.h5', + ... ] + >>> ipx.Read(list_of_files) # doctest: +SKIP """ @@ -306,55 +325,106 @@ def __init__( self, data_source=None, product=None, - filename_pattern="ATL{product:2}_{datetime:%Y%m%d%H%M%S}_{rgt:4}{cycle:2}{orbitsegment:2}_{version:3}_{revision:2}.h5", + filename_pattern=None, catalog=None, + glob_kwargs={}, out_obj_type=None, # xr.Dataset, ): - # Raise error for depreciated argument + # Raise error for deprecated argument if catalog: raise DeprecationError( - 'The `catalog` argument has been deprecated and intake is no longer supported. ' - 'Please use the `data_source` argument to specify your dataset instead.' + "The `catalog` argument has been deprecated and intake is no longer supported. " + "Please use the `data_source` argument to specify your dataset instead." ) if data_source is None: - raise ValueError("Please provide a data source.") - else: - self._source_type = _check_datasource(data_source) - self.data_source = data_source + raise ValueError("data_source is a required arguemnt") - if product is None: - raise ValueError( - "Please provide the ICESat-2 data product of your file(s)." + # Raise warnings for deprecated arguments + if filename_pattern: + warnings.warn( + "The `filename_pattern` argument is deprecated. Instead please provide a " + "string, list, or glob string to the `data_source` argument.", + stacklevel=2, ) - else: - self._prod = is2ref._validate_product(product) - pattern_ck, filelist = Read._check_source_for_pattern( - data_source, filename_pattern - ) - assert pattern_ck - # Note: need to check if this works for subset and non-subset NSIDC files (processed_ prepends the former) - self._pattern = filename_pattern - - # this is a first pass at getting rid of mixed product types and warning the user. - # it takes an approach assuming the product name is in the filename, but needs reworking if we let multiple products be loaded - # one way to handle this would be bring in the product info during the loading step and fill in product there instead of requiring it from the user - filtered_filelist = [file for file in filelist if self._prod in file] - if len(filtered_filelist) == 0: + + if product: + product = is2ref._validate_product(product) warnings.warn( - "Your filenames do not contain a product identifier (e.g. ATL06). " - "You will likely need to manually merge your dataframes." + "The `product` argument is no longer required. If the `data_source` argument given " + "contains files with multiple products the `product` argument will be used " + "to filter that list. In all other cases the product argument is ignored. " + "The recommended approach is to not include a `product` argument and instead " + "provide a `data_source` with files of only a single product type`.", + stacklevel=2, ) + + # Create the filelist from the `data_source` argument + if filename_pattern: + # maintained for backward compatibility + pattern_ck, filelist = Read._check_source_for_pattern( + data_source, filename_pattern + ) + assert pattern_ck self._filelist = filelist - elif len(filtered_filelist) < len(filelist): - warnings.warn( - "Some files matching your filename pattern were removed as they were not the specified product." + elif isinstance(data_source, list): + self._filelist = data_source + elif os.path.isdir(data_source): + data_source = os.path.join(data_source, "*") + self._filelist = glob.glob(data_source, **glob_kwargs) + else: + self._filelist = glob.glob(data_source, **glob_kwargs) + # Remove any directories from the list + self._filelist = [f for f in self._filelist if not os.path.isdir(f)] + + # Create a dictionary of the products as read from the metadata + product_dict = {} + for file_ in self._filelist: + product_dict[file_] = self._extract_product(file_) + + # Raise warnings or errors for multiple products or products not matching the user-specified product + all_products = list(set(product_dict.values())) + if len(all_products) > 1: + if product: + warnings.warn( + f"Multiple products found in list of files: {product_dict}. Files that " + "do not match the user specified product will be removed from processing.\n" + "Filtering files using a `product` argument is deprecated. Please use the " + "`data_source` argument to specify a list of files with the same product.", + stacklevel=2, + ) + self._filelist = [] + for key, value in product_dict.items(): + if value == product: + self._filelist.append(key) + if len(self._filelist) == 0: + raise TypeError( + "No files found in the file list matching the user-specified " + "product type" + ) + # Use the cleaned filelist to assign a product + self._product = product + else: + raise TypeError( + f"Multiple product types were found in the file list: {product_dict}." + "Please provide a valid `data_source` parameter indicating files of a single " + "product" + ) + elif len(all_products) == 0: + raise TypeError( + "No files found matching the specified `data_source`. Check your glob " + "string or file list." ) - self._filelist = filtered_filelist else: - self._filelist = filelist - - # after validation, use the notebook code and code outline to start implementing the rest of the class + # Assign the identified product to the property + self._product = all_products[0] + # Raise a warning if the metadata-located product differs from the user-specified product + if product and self._product != product: + warnings.warn( + f"User specified product {product} does not match the product from the file" + " metadata {self._product}", + stacklevel=2, + ) if out_obj_type is not None: print( @@ -387,14 +457,43 @@ def vars(self): if not hasattr(self, "_read_vars"): self._read_vars = Variables( - "file", path=self._filelist[0], product=self._prod + "file", path=self.filelist[0], product=self.product ) return self._read_vars + @property + def filelist(self): + """ + Return the list of files represented by this Read object. + """ + return self._filelist + + @property + def product(self): + """ + Return the product associated with the Read object. + """ + return self._product + # ---------------------------------------------------------------------- # Methods + @staticmethod + def _extract_product(filepath): + """ + Read the product type from the metadata of the file. Return the product as a string. + """ + with h5py.File(filepath, "r") as f: + try: + product = f.attrs["short_name"].decode() + product = is2ref._validate_product(product) + except KeyError: + raise AttributeError( + f"Unable to extract the product name from file metadata." + ) + return product + @staticmethod def _check_source_for_pattern(source, filename_pattern): """ @@ -654,7 +753,7 @@ def load(self): # However, this led to errors when I tried to combine two identical datasets because the single dimension was equal. # In these situations, xarray recommends manually controlling the merge/concat process yourself. # While unlikely to be a broad issue, I've heard of multiple matching timestamps causing issues for combining multiple IS2 datasets. - for file in self._filelist: + for file in self.filelist: all_dss.append( self._build_single_file_dataset(file, groups_list) ) # wanted_groups, vgrp.keys())) @@ -689,7 +788,7 @@ def _build_dataset_template(self, file): gran_idx=[np.uint64(999999)], source_file=(["gran_idx"], [file]), ), - attrs=dict(data_product=self._prod), + attrs=dict(data_product=self.product), ) return is2ds @@ -737,20 +836,11 @@ def _build_single_file_dataset(self, file, groups_list): ------- Xarray Dataset """ - file_product = self._read_single_grp(file, "/").attrs["identifier_product_type"] - assert ( - file_product == self._prod - ), "Your product specification does not match the product specification within your files." - # I think the below method might NOT read the file into memory as the above might? - # import h5py - # with h5py.File(filepath,'r') as h5pt: - # prod_id = h5pt.attrs["identifier_product_type"] - # DEVNOTE: if and elif does not actually apply wanted variable list, and has not been tested for merging multiple files into one ds # if a gridded product # TODO: all products need to be tested, and quicklook products added or explicitly excluded # Level 3b, gridded (netcdf): ATL14, 15, 16, 17, 18, 19, 20, 21 - if self._prod in [ + if self.product in [ "ATL14", "ATL15", "ATL16", @@ -764,7 +854,7 @@ def _build_single_file_dataset(self, file, groups_list): is2ds = xr.open_dataset(file) # Level 3b, hdf5: ATL11 - elif self._prod in ["ATL11"]: + elif self.product in ["ATL11"]: is2ds = self._build_dataset_template(file) # returns the wanted groups as a single list of full group path strings diff --git a/icepyx/tests/test_is2ref.py b/icepyx/tests/test_is2ref.py index b22709c98..fb8d16cad 100644 --- a/icepyx/tests/test_is2ref.py +++ b/icepyx/tests/test_is2ref.py @@ -8,14 +8,14 @@ def test_num_product(): dsnum = 6 - ermsg = "Please enter a product string" + ermsg = "A valid product string was not provided. Check user input, if given, or file metadata." with pytest.raises(TypeError, match=ermsg): is2ref._validate_product(dsnum) def test_bad_product(): wrngds = "atl-6" - ermsg = "Please enter a valid product" + ermsg = "A valid product string was not provided. Check user input, if given, or file metadata." with pytest.raises(AssertionError, match=ermsg): is2ref._validate_product(wrngds) From aedbcce20d851209f3cf15bea6993efbcc2984fe Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Thu, 19 Oct 2023 18:13:18 -0400 Subject: [PATCH 081/124] enable QUEST kwarg handling (#452) * add kwarg acceptance for data queries and download_all in quest * Add QUEST dataset page to RTD --------- Co-authored-by: zachghiaccio --- doc/source/index.rst | 1 + icepyx/quest/quest.py | 99 ++++++++++++++++++++++++++++---------- icepyx/tests/test_quest.py | 31 ++++++++++-- 3 files changed, 102 insertions(+), 29 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 719f528b2..586c8810f 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -146,6 +146,7 @@ ICESat-2 datasets to enable scientific discovery. contributing/contribution_guidelines contributing/how_to_contribute contributing/icepyx_internals + contributing/quest-available-datasets contributing/attribution_link contributing/development_plan contributing/release_guide diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index c54e49b73..fe3039a39 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -59,7 +59,7 @@ def __init__( date_range=None, start_time=None, end_time=None, - proj="Default", + proj="default", ): """ Tells QUEST to initialize data given the user input spatiotemporal data. @@ -94,9 +94,23 @@ def add_icesat2( tracks=None, files=None, **kwargs, - ): + ) -> None: """ Adds ICESat-2 datasets to QUEST structure. + + Parameters + ---------- + + For details on inputs, see the Query documentation. + + Returns + ------- + None + + See Also + -------- + icepyx.core.GenQuery + icepyx.core.Query """ query = Query( @@ -122,41 +136,76 @@ def add_icesat2( # ---------------------------------------------------------------------- # Methods (on all datasets) - # error handling? what happens when one of i fails... - def search_all(self): + # error handling? what happens when the user tries to re-query? + def search_all(self, **kwargs): """ Searches for requred dataset within platform (i.e. ICESat-2, Argo) of interest. + + Parameters + ---------- + **kwargs : default None + Optional passing of keyword arguments to supply additional search constraints per datasets. + Each key must match the dataset name (e.g. "icesat2", "argo") as in quest.datasets.keys(), + and the value is a dictionary of acceptable keyword arguments + and values allowable for the `search_data()` function for that dataset. + For instance: `icesat2 = {"IDs":True}, argo = {"presRange":"10,500"}`. """ print("\nSearching all datasets...") - for i in self.datasets.values(): + for k, v in self.datasets.items(): print() try: - # querying ICESat-2 data - if isinstance(i, Query): + if isinstance(v, Query): print("---ICESat-2---") - msg = i.avail_granules() + try: + msg = v.avail_granules(kwargs[k]) + except KeyError: + msg = v.avail_granules() print(msg) - else: # querying all other data sets - print(i) - i.search_data() + else: + print(k) + try: + v.search_data(kwargs[k]) + except KeyError: + v.search_data() except: - dataset_name = type(i).__name__ + dataset_name = type(v).__name__ print("Error querying data from {0}".format(dataset_name)) - # error handling? what happens when one of i fails... - def download_all(self, path=""): - ' ' 'Downloads requested dataset(s).' ' ' + # error handling? what happens if the user tries to re-download? + def download_all(self, path="", **kwargs): + """ + Downloads requested dataset(s). + + Parameters + ---------- + **kwargs : default None + Optional passing of keyword arguments to supply additional search constraints per datasets. + Each key must match the dataset name (e.g. "icesat2", "argo") as in quest.datasets.keys(), + and the value is a dictionary of acceptable keyword arguments + and values allowable for the `search_data()` function for that dataset. + For instance: `icesat2 = {"verbose":True}, argo = {"keep_existing":True}`. + """ print("\nDownloading all datasets...") - for i in self.datasets.values(): + for k, v in self.datasets.items(): print() - if isinstance(i, Query): - print("---ICESat-2---") - msg = i.download_granules(path) - print(msg) - else: - i.download() - print(i) - - # DEVNOTE: see colocated data branch and phyto team files for code that expands quest functionality + try: + + if isinstance(v, Query): + print("---ICESat-2---") + try: + msg = v.download_granules(path, kwargs[k]) + except KeyError: + msg = v.download_granules(path) + print(msg) + else: + print(k) + try: + msg = v.download(kwargs[k]) + except KeyError: + msg = v.download() + print(msg) + except: + dataset_name = type(v).__name__ + print("Error downloading data from {0}".format(dataset_name)) diff --git a/icepyx/tests/test_quest.py b/icepyx/tests/test_quest.py index 043ee159e..f50b1bea2 100644 --- a/icepyx/tests/test_quest.py +++ b/icepyx/tests/test_quest.py @@ -68,13 +68,36 @@ def test_add_is2(quest_instance): ########## ALL DATASET METHODS TESTS ########## -# is successful execution enough here? # each of the query functions should be tested in their respective modules def test_search_all(quest_instance): # Search and test all datasets quest_instance.search_all() -def test_download_all(): - # this will require auth in some cases... - pass +@pytest.mark.parametrize( + "kwargs", + [ + {"icesat2": {"IDs": True}}, + # {"argo":{"presRange":"10,500"}}, + # {"icesat2":{"IDs":True}, "argo":{"presRange":"10,500"}} + ], +) +def test_search_all_kwargs(quest_instance, kwargs): + quest_instance.search_all(**kwargs) + + +# TESTS NOT IMPLEMENTED +# def test_download_all(): +# # this will require auth in some cases... +# pass + +# @pytest.mark.parametrize( +# "kwargs", +# [ +# {"icesat2": {"verbose":True}}, +# # {"argo":{"keep_existing":True}, +# # {"icesat2":{"verbose":True}, "argo":{"keep_existing":True} +# ], +# ) +# def test_download_all_kwargs(quest_instance, kwargs): +# pass From 73f929e8c1a50e43f6346aa69cfb607cd5d96e67 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 11:41:03 -0400 Subject: [PATCH 082/124] docs: add rwegener2 as a contributor for bug, code, and 6 more (#460) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Jessica Scheick --- .all-contributorsrc | 19 ++++++++++++++++++- CONTRIBUTORS.rst | 11 ++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 8f9a076e4..3b321715a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -422,6 +422,22 @@ "contributions": [ "review" ] + }, + { + "login": "rwegener2", + "name": "Rachel Wegener", + "avatar_url": "https://avatars.githubusercontent.com/u/35503632?v=4", + "profile": "https://rwegener2.github.io/", + "contributions": [ + "bug", + "code", + "doc", + "ideas", + "maintenance", + "review", + "test", + "tutorial" + ] } ], "contributorsPerLine": 7, @@ -430,5 +446,6 @@ "repoType": "github", "repoHost": "https://github.com", "skipCi": true, - "commitConvention": "angular" + "commitConvention": "angular", + "commitType": "docs" } diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index c6b0c84f5..be362bb28 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -31,41 +31,42 @@ Thanks goes to these wonderful people (`emoji key Nicole Abib
Nicole Abib
💻 🤔 + Rachel Wegener
Rachel Wegener

🐛 💻 📖 🤔 🚧 👀 ⚠️ Raphael Hagen
Raphael Hagen

📖 🎨 💻 🚇 👀 Romina Piunno
Romina Piunno

💻 🤔 🧑‍🏫 👀 Sarah Hall
Sarah Hall

🐛 💻 📖 🚧 ⚠️ Scott Henderson
Scott Henderson

🚧 Sebastian Alvis
Sebastian Alvis

📖 🚇 Shashank Bhushan
Shashank Bhushan

💡 - Tian Li
Tian Li

🐛 💻 📖 💡 🤔 👀 ⚠️ 🔧 + Tian Li
Tian Li

🐛 💻 📖 💡 🤔 👀 ⚠️ 🔧 Tom Johnson
Tom Johnson

📖 🚇 Tyler Sutterley
Tyler Sutterley

📖 💻 🤔 💬 🛡️ ⚠️ Wei Ji
Wei Ji

🐛 💻 📖 💡 🤔 🚇 🚧 🧑‍🏫 💬 👀 ⚠️ 📢 Wilson Sauthoff
Wilson Sauthoff

👀 Zach Fair
Zach Fair

🐛 💻 📖 🤔 💬 👀 alexdibella
alexdibella

🐛 🤔 💻 - bidhya
bidhya

💡 + bidhya
bidhya

💡 learn2phoenix
learn2phoenix

💻 liuzheng-arctic
liuzheng-arctic

📖 🐛 💻 🤔 👀 🔧 💡 nitin-ravinder
nitin-ravinder

🐛 👀 ravindraK08
ravindraK08

👀 smithb
smithb

🤔 tedmaksym
tedmaksym

🤔 - trevorskaggs
trevorskaggs

🐛 💻 + trevorskaggs
trevorskaggs

🐛 💻 trey-stafford
trey-stafford

💻 🤔 🚧 👀 💬 - + - + This project follows the `all-contributors `_ specification. Contributions of any kind welcome! From a56a9c8864ca23d73850f9e25902c80d75634120 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 12:07:55 -0400 Subject: [PATCH 083/124] docs: add jpswinski as a contributor for review (#461) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Jessica Scheick --- .all-contributorsrc | 3 ++- CONTRIBUTORS.rst | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3b321715a..85b5486b9 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -382,7 +382,8 @@ "avatar_url": "https://avatars.githubusercontent.com/u/54070345?v=4", "profile": "https://github.com/jpswinski", "contributions": [ - "code" + "code", + "review" ] }, { diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index be362bb28..337ff6661 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -23,7 +23,7 @@ Thanks goes to these wonderful people (`emoji key Fernando Perez
Fernando Perez

🎨 💼 🤔 - JP Swinski
JP Swinski

💻 + JP Swinski
JP Swinski

💻 👀 Jessica
Jessica

🐛 💻 🖋 📖 🎨 💡 🤔 🚧 🧑‍🏫 📆 💬 👀 Joachim Meyer
Joachim Meyer

🧑‍🏫 🚧 Kelsey Bisson
Kelsey Bisson

🐛 💻 📖 🤔 💡 🤔 🧑‍🏫 💬 👀 From bdcc9bddd81060ced54ffbd323b0dc635ee368de Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:13:58 -0400 Subject: [PATCH 084/124] docs: add whyjz as a contributor for tutorial (#462) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Jessica Scheick --- .all-contributorsrc | 9 +++++++++ CONTRIBUTORS.rst | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 85b5486b9..6b24eac03 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -439,6 +439,15 @@ "test", "tutorial" ] + }, + { + "login": "whyjz", + "name": "Whyjay Zheng", + "avatar_url": "https://avatars.githubusercontent.com/u/19339926?v=4", + "profile": "https://whyjz.github.io/", + "contributions": [ + "tutorial" + ] } ], "contributorsPerLine": 7, diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 337ff6661..1fd8bab42 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -44,20 +44,21 @@ Thanks goes to these wonderful people (`emoji key Tom Johnson
Tom Johnson

📖 🚇 Tyler Sutterley
Tyler Sutterley

📖 💻 🤔 💬 🛡️ ⚠️ Wei Ji
Wei Ji

🐛 💻 📖 💡 🤔 🚇 🚧 🧑‍🏫 💬 👀 ⚠️ 📢 + Whyjay Zheng
Whyjay Zheng

Wilson Sauthoff
Wilson Sauthoff

👀 Zach Fair
Zach Fair

🐛 💻 📖 🤔 💬 👀 - alexdibella
alexdibella

🐛 🤔 💻 + alexdibella
alexdibella

🐛 🤔 💻 bidhya
bidhya

💡 learn2phoenix
learn2phoenix

💻 liuzheng-arctic
liuzheng-arctic

📖 🐛 💻 🤔 👀 🔧 💡 nitin-ravinder
nitin-ravinder

🐛 👀 ravindraK08
ravindraK08

👀 smithb
smithb

🤔 - tedmaksym
tedmaksym

🤔 + tedmaksym
tedmaksym

🤔 trevorskaggs
trevorskaggs

🐛 💻 trey-stafford
trey-stafford

💻 🤔 🚧 👀 💬 From f514619ec30b8a41fea62849daf12b263c173ab4 Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Tue, 31 Oct 2023 15:25:07 +0000 Subject: [PATCH 085/124] Link to QUEST dataset page went missing again. Fixed. --- doc/source/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index 719f528b2..7b4cb6f11 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -145,6 +145,7 @@ ICESat-2 datasets to enable scientific discovery. contributing/contributors_link contributing/contribution_guidelines contributing/how_to_contribute + contributing/quest_supported_label contributing/icepyx_internals contributing/attribution_link contributing/development_plan From 53831aae133918f92d6d279747db10acc33f207e Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Tue, 31 Oct 2023 15:35:41 +0000 Subject: [PATCH 086/124] Fixed typo in reference. --- doc/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 7b4cb6f11..1f5728691 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -145,7 +145,7 @@ ICESat-2 datasets to enable scientific discovery. contributing/contributors_link contributing/contribution_guidelines contributing/how_to_contribute - contributing/quest_supported_label + contributing/quest-available-datasets contributing/icepyx_internals contributing/attribution_link contributing/development_plan From a0c5acd341a141f0a89bf5276772d3c3171fa84d Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Tue, 31 Oct 2023 16:17:22 +0000 Subject: [PATCH 087/124] Moved Argo workflow to Examples folder. --- .../contributing/quest-available-datasets.rst | 14 +++++++++----- .../example_notebooks/QUEST_argo_data_access.ipynb | 0 2 files changed, 9 insertions(+), 5 deletions(-) rename argo_workflow.ipynb => doc/source/example_notebooks/QUEST_argo_data_access.ipynb (100%) diff --git a/doc/source/contributing/quest-available-datasets.rst b/doc/source/contributing/quest-available-datasets.rst index 91a6283a0..6c8d86f15 100644 --- a/doc/source/contributing/quest-available-datasets.rst +++ b/doc/source/contributing/quest-available-datasets.rst @@ -9,10 +9,14 @@ On this page, we outline the datasets that are supported by the QUEST module. Cl List of Datasets ---------------- -* `Argo `_ - * The Argo mission involves a series of floats that are designed to capture vertical ocean profiles of temperature, salinity, and pressure down to ~2000 m. Some floats are in support of BGC-Argo, which also includes data relevant for biogeochemical applications: oxygen, nitrate, chlorophyll, backscatter, and solar irradiance. - * (Link Kelsey's paper here) - * (Link to example workbook here) +`Argo `_ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The Argo mission involves a series of floats that are designed to capture vertical ocean profiles of temperature, salinity, and pressure down to ~2000 m. Some floats are in support of BGC-Argo, which also includes data relevant for biogeochemical applications: oxygen, nitrate, chlorophyll, backscatter, and solar irradiance. + +A paper outlining the Argo extension to QUEST is currently in preparation, with a citable preprint available in the near future. + +:ref:`Argo Workflow Example` +(Link to example workbook here) Adding a Dataset to QUEST @@ -22,4 +26,4 @@ Want to add a new dataset to QUEST? No problem! QUEST includes a template script Guidelines on how to construct your dataset module may be found here: (link to be added) -Once you have developed a script with the template, you may request for the module to be added to QUEST via Github. Please see the How to Contribute page :ref:`dev_guide_label` for instructions on how to contribute to icepyx. \ No newline at end of file +Once you have developed a script with the template, you may request for the module to be added to QUEST via Github. Please see the How to Contribute page :ref:`dev_guide_label` for instructions on how to contribute to icepyx. diff --git a/argo_workflow.ipynb b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb similarity index 100% rename from argo_workflow.ipynb rename to doc/source/example_notebooks/QUEST_argo_data_access.ipynb From a326949d3a1e19e8a15a7cad49e4b5211a2f3e99 Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Tue, 31 Oct 2023 16:51:30 +0000 Subject: [PATCH 088/124] Add link to Argo workbook. --- doc/source/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/index.rst b/doc/source/index.rst index 1f5728691..baedac53e 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -128,6 +128,7 @@ ICESat-2 datasets to enable scientific discovery. example_notebooks/IS2_data_visualization example_notebooks/IS2_data_read-in example_notebooks/IS2_cloud_data_access + example_notebooks/QUEST_argo_data_access .. toctree:: :maxdepth: 2 From 38e9b5d553e69a5510728b043bdc90758b8643bd Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 1 Nov 2023 11:50:33 -0400 Subject: [PATCH 089/124] test argo script via a quest instance --- icepyx/tests/test_quest_argo.py | 47 +++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index 652bc2470..25871805f 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -1,11 +1,19 @@ import pytest import re -from icepyx.quest.dataset_scripts.argo import Argo +from icepyx.quest.quest import Quest + +# create an Argo instance via quest (Argo is a submodule) +def argo_quest_instance(bounding_box, date_range, params=None): + my_quest = Quest(spatial_extent=bounding_box, date_range=date_range) + my_quest.add_argo() + my_argo = my_quest.datasets["argo"] + + return my_argo def test_available_profiles(): - reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) obs_msg = reg_a.search_data() exp_msg = "19 valid profiles have been identified" @@ -14,7 +22,7 @@ def test_available_profiles(): def test_no_available_profiles(): - reg_a = Argo([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) + reg_a = argo_quest_instance([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) obs = reg_a.search_data() exp = ( @@ -25,7 +33,7 @@ def test_no_available_profiles(): def test_fmt_coordinates(): - reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) obs = reg_a._fmt_coordinates() exp = "[[-143.0,30.0],[-143.0,37.0],[-154.0,37.0],[-154.0,30.0],[-143.0,30.0]]" @@ -34,7 +42,7 @@ def test_fmt_coordinates(): def test_invalid_param(): - reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) invalid_params = ["temp", "temperature_files"] @@ -49,9 +57,8 @@ def test_invalid_param(): def test_download_parse_into_df(): - reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) - # reg_a.search_data() - reg_a.get_dataframe(params=["salinity"]) # note: pressure is returned by default + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) + reg_a.download(params=["salinity"]) # note: pressure is returned by default obs_cols = reg_a.argodata.columns @@ -67,32 +74,32 @@ def test_download_parse_into_df(): assert set(exp_cols) == set(obs_cols) - assert len(reg_a.argodata) == 1943 + assert len(reg_a.argodata) == 1942 + + +# approach for additional testing of df functions: create json files with profiles and store them in test suite +# then use those for the comparison (e.g. number of rows in df and json match) def test_merge_df(): - reg_a = Argo([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) + reg_a = argo_quest_instance([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) param_list = ["salinity", "temperature", "down_irradiance412"] - df = reg_a.get_dataframe(params=param_list) + df = reg_a.download(params=param_list) assert "down_irradiance412" in df.columns assert "down_irradiance412_argoqc" in df.columns - df = reg_a.get_dataframe(["doxy"], keep_existing=True) + df = reg_a.download(["doxy"], keep_existing=True) assert "doxy" in df.columns assert "doxy_argoqc" in df.columns assert "down_irradiance412" in df.columns assert "down_irradiance412_argoqc" in df.columns -""" def test_presRange_input_param(): - reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) - reg_a.get_dataframe(params=["salinity"], presRange="0.2,100") - -""" + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) + df = reg_a.download(params=["salinity"], presRange="0.2,100") -# goal: check number of rows in df matches rows in json -# approach: create json files with profiles and store them in test suite -# then use those for the comparison + assert df["pressure"].min() >= 0.2 + assert df["pressure"].max() <= 100 From 992e7aeda61dee4fbd66b06f5f0cd25d1df6a918 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 1 Nov 2023 11:52:15 -0400 Subject: [PATCH 090/124] clean up argo script --- icepyx/quest/dataset_scripts/argo.py | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 60343e125..0319761cb 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -227,7 +227,7 @@ def search_data(self, params=None, printURL=False) -> str: # if new search is called with additional parameters if not params is None: - self.params.append(self._validate_parameters(params)) + self.params.extend(self._validate_parameters(params)) # to remove duplicated from list self.params = list(set(self.params)) @@ -409,7 +409,7 @@ def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFr # if new search is called with additional parameters if not params is None: - self.params.append(self._validate_parameters(params)) + self.params.extend(self._validate_parameters(params)) # to remove duplicated from list self.params = list(set(self.params)) else: @@ -452,27 +452,3 @@ def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFr self.argodata.reset_index(inplace=True, drop=True) return self.argodata - - -# this is just for the purpose of debugging and should be removed later (after being turned into tests) -if __name__ == "__main__": - # no search results - # reg_a = Argo([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) - # profiles available - # reg_a = Argo([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) # "2022-04-26"]) - - # bgc profiles available - reg_a = Argo([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) - - param_list = ["down_irradiance412"] - bad_param = ["up_irradiance412"] - # param_list = ["doxy"] - - # reg_a.search_data(params=bad_param, printURL=True) - - reg_a.download(params=param_list) - - reg_a.download(params=["doxy"], keep_existing=True) # , presRange="0.2,100" - # ) - - print(reg_a) From ea0d37e9715f57921dd26c47802c7d242c9c2046 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 1 Nov 2023 12:06:07 -0400 Subject: [PATCH 091/124] clean up quest tests --- icepyx/tests/test_quest.py | 39 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/icepyx/tests/test_quest.py b/icepyx/tests/test_quest.py index cd23864c1..b044b03ba 100644 --- a/icepyx/tests/test_quest.py +++ b/icepyx/tests/test_quest.py @@ -18,7 +18,6 @@ def quest_instance(scope="module", autouse=True): # Paramaterize these add_dataset tests once more datasets are added def test_add_is2(quest_instance): # Add ATL06 as a test to QUEST - prod = "ATL06" quest_instance.add_icesat2(product=prod) exp_key = "icesat2" @@ -46,37 +45,31 @@ def test_add_argo(quest_instance): assert quest_instance.datasets[exp_key].params == params -def test_add_multiple_datasets(): - bounding_box = [-150, 30, -120, 60] - date_range = ["2022-06-07", "2022-06-14"] - my_quest = Quest(spatial_extent=bounding_box, date_range=date_range) - - # print(my_quest.spatial) - # print(my_quest.temporal) - - # my_quest.add_argo(params=["down_irradiance412", "temperature"]) - # print(my_quest.datasets["argo"].params) +def test_add_multiple_datasets(quest_instance): + quest_instance.add_argo(params=["down_irradiance412", "temperature"]) + # print(quest_instance.datasets["argo"].params) - my_quest.add_icesat2(product="ATL06") - # print(my_quest.datasets["icesat2"].product) + quest_instance.add_icesat2(product="ATL06") + # print(quest_instance.datasets["icesat2"].product) - print(my_quest) - - # my_quest.search_all() - # - # # this one still needs work for IS2 because of auth... - # my_quest.download_all() + exp_keys = ["argo", "icesat2"] + assert set(exp_keys) == set(quest_instance.datasets.keys()) ########## ALL DATASET METHODS TESTS ########## -# is successful execution enough here? # each of the query functions should be tested in their respective modules def test_search_all(quest_instance): + quest_instance.add_argo(params=["down_irradiance412", "temperature"]) + quest_instance.add_icesat2(product="ATL06") + # Search and test all datasets quest_instance.search_all() -def test_download_all(): - # this will require auth in some cases... - pass +# def test_download_all(): +# quest_instance.add_argo(params=["down_irradiance412", "temperature"]) +# quest_instance.add_icesat2(product="ATL06") +# +# # this will require auth in some cases... +# quest_instance.download_all() From cce8fa70ef98d4ad4758269e017df753c71bdfe6 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 1 Nov 2023 12:06:45 -0400 Subject: [PATCH 092/124] remove =None from required inputs --- icepyx/quest/dataset_scripts/dataset.py | 4 +--- icepyx/quest/quest.py | 10 ++++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index e76081e08..884ec46ea 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -11,9 +11,7 @@ class DataSet: All sub-classes must support the following methods for use via the QUEST class. """ - def __init__( - self, spatial_extent=None, date_range=None, start_time=None, end_time=None - ): + def __init__(self, spatial_extent, date_range, start_time=None, end_time=None): """ Complete any dataset specific initializations (i.e. beyond space and time) required here. For instance, ICESat-2 requires a product, and Argo requires parameters. diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index 43ac0db0a..aab5640ed 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -21,10 +21,12 @@ class Quest(GenQuery): Geospatial projection. Not yet implemented + Returns ------- quest object + Examples -------- Initializing Quest with a bounding box. @@ -54,8 +56,8 @@ class Quest(GenQuery): def __init__( self, - spatial_extent=None, - date_range=None, + spatial_extent, + date_range, start_time=None, end_time=None, proj="Default", @@ -86,7 +88,7 @@ def __str__(self): def add_icesat2( self, - product=None, + product, start_time=None, end_time=None, version=None, @@ -94,7 +96,7 @@ def add_icesat2( tracks=None, files=None, **kwargs, - ): + ) -> None: """ Adds ICESat-2 datasets to QUEST structure. """ From efcb16d938f98893f50654a864e43f4a9dd5f927 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 1 Nov 2023 12:24:28 -0400 Subject: [PATCH 093/124] remove note from argo --- icepyx/quest/quest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index aab5640ed..f915703c5 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -175,5 +175,3 @@ def download_all(self, path=""): else: i.download() print(i) - - # DEVNOTE: see colocated data branch and phyto team files for code that expands quest functionality From fb90b0c6b18e96379ab2468b3aa2f3f4d27de6ee Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Thu, 2 Nov 2023 10:04:30 -0400 Subject: [PATCH 094/124] add newest icepyx citations (#455) --- doc/source/tracking/citations.rst | 2 ++ doc/source/tracking/icepyx_pubs.bib | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/doc/source/tracking/citations.rst b/doc/source/tracking/citations.rst index b31132be8..bf5672587 100644 --- a/doc/source/tracking/citations.rst +++ b/doc/source/tracking/citations.rst @@ -49,6 +49,8 @@ Research that utilizes icepyx for ICESat-2 data .. bibliography:: icepyx_pubs.bib :style: mystyle + Freer2023 + Idestrom2023 Shean2023 Eidam2022 Leeuwen:2022 diff --git a/doc/source/tracking/icepyx_pubs.bib b/doc/source/tracking/icepyx_pubs.bib index a1d945c01..d13c9653f 100644 --- a/doc/source/tracking/icepyx_pubs.bib +++ b/doc/source/tracking/icepyx_pubs.bib @@ -183,6 +183,30 @@ @inProceedings{Fernando:2021 } +@Article{Freer2023, +AUTHOR = {Freer, B. I. D. and Marsh, O. J. and Hogg, A. E. and Fricker, H. A. and Padman, L.}, +TITLE = {Modes of {Antarctic} tidal grounding line migration revealed by {Ice, Cloud, and land Elevation Satellite-2 (ICESat-2)} laser altimetry}, +JOURNAL = {The Cryosphere}, +VOLUME = {17}, +YEAR = {2023}, +NUMBER = {9}, +PAGES = {4079--4101}, +URL = {https://tc.copernicus.org/articles/17/4079/2023/}, +DOI = {10.5194/tc-17-4079-2023} +} + + +@mastersthesis{Idestrom2023, + author = {Petter Idestr\"{o}m}, + title = {Remote Sensing of Cryospheric Surfaces: Small Scale Surface Roughness Signatures in Satellite Altimetry Data}, + school = {Ume\aa University}, + year = {2023}, + address = {Sweden}, + month = {Sept.}, + url = {https://www.diva-portal.org/smash/get/diva2:1801057/FULLTEXT01.pdf} +} + + @misc{Leeuwen:2022, author = {van Leeuwen, Gijs}, title = {The automated retrieval of supraglacial lake depth and extent from {ICESat-2} photon clouds leveraging {DBSCAN} clustering}, From ce29a55eab27e36ee8b5280947d0f0e93ef7c318 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Fri, 3 Nov 2023 15:46:55 -0400 Subject: [PATCH 095/124] remove unused test data file --- icepyx/tests/argovis_test_data.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 icepyx/tests/argovis_test_data.json diff --git a/icepyx/tests/argovis_test_data.json b/icepyx/tests/argovis_test_data.json deleted file mode 100644 index f255b10aa..000000000 --- a/icepyx/tests/argovis_test_data.json +++ /dev/null @@ -1 +0,0 @@ -[{"_id":"4902461_166","POSITIONING_SYSTEM":"GPS","DATA_CENTRE":"ME","PI_NAME":"Blair Greenan","WMO_INST_TYPE":"865","VERTICAL_SAMPLING_SCHEME":"Primary sampling: averaged","DATA_MODE":"A","PLATFORM_TYPE":"NOVA","measurements":[{"pres":1.7,"psal":32.485,"temp":4.558},{"pres":2.5,"psal":32.485,"temp":4.558},{"pres":3.6,"psal":32.485,"temp":4.558},{"pres":4.6,"psal":32.485,"temp":4.558},{"pres":5.6,"psal":32.485,"temp":4.559},{"pres":6.6,"psal":32.485,"temp":4.56},{"pres":7.6,"psal":32.485,"temp":4.56},{"pres":8.6,"psal":32.485,"temp":4.56},{"pres":9.7,"psal":32.485,"temp":4.561},{"pres":10.7,"psal":32.485,"temp":4.56},{"pres":11.6,"psal":32.485,"temp":4.561},{"pres":12.5,"psal":32.485,"temp":4.56},{"pres":13.6,"psal":32.485,"temp":4.561},{"pres":14.6,"psal":32.485,"temp":4.561},{"pres":15.6,"psal":32.485,"temp":4.56},{"pres":16.6,"psal":32.485,"temp":4.561},{"pres":17.5,"psal":32.486,"temp":4.561},{"pres":18.6,"psal":32.485,"temp":4.561},{"pres":19.6,"psal":32.486,"temp":4.56},{"pres":20.6,"psal":32.485,"temp":4.561},{"pres":21.6,"psal":32.485,"temp":4.56},{"pres":22.7,"psal":32.484,"temp":4.561},{"pres":23.6,"psal":32.485,"temp":4.561},{"pres":24.6,"psal":32.485,"temp":4.561},{"pres":25.6,"psal":32.485,"temp":4.561},{"pres":26.6,"psal":32.485,"temp":4.562},{"pres":27.5,"psal":32.486,"temp":4.562},{"pres":28.6,"psal":32.486,"temp":4.562},{"pres":29.6,"psal":32.486,"temp":4.563},{"pres":30.6,"psal":32.486,"temp":4.563},{"pres":31.6,"psal":32.486,"temp":4.563},{"pres":32.7,"psal":32.497,"temp":4.561},{"pres":33.6,"psal":32.502,"temp":4.561},{"pres":34.6,"psal":32.503,"temp":4.56},{"pres":35.6,"psal":32.506,"temp":4.561},{"pres":36.5,"psal":32.517,"temp":4.561},{"pres":37.6,"psal":32.52,"temp":4.561},{"pres":38.6,"psal":32.524,"temp":4.562},{"pres":39.6,"psal":32.534,"temp":4.563},{"pres":40.6,"psal":32.548,"temp":4.547},{"pres":41.6,"psal":32.548,"temp":4.53},{"pres":42.6,"psal":32.553,"temp":4.517},{"pres":43.6,"psal":32.554,"temp":4.498},{"pres":44.6,"psal":32.555,"temp":4.488},{"pres":45.6,"psal":32.557,"temp":4.482},{"pres":46.6,"psal":32.557,"temp":4.481},{"pres":47.6,"psal":32.558,"temp":4.48},{"pres":48.6,"psal":32.559,"temp":4.481},{"pres":49.6,"psal":32.561,"temp":4.479},{"pres":50.6,"psal":32.567,"temp":4.479},{"pres":51.6,"psal":32.571,"temp":4.477},{"pres":52.6,"psal":32.575,"temp":4.476},{"pres":53.6,"psal":32.583,"temp":4.488},{"pres":54.6,"psal":32.606,"temp":4.541},{"pres":55.6,"psal":32.627,"temp":4.546},{"pres":56.6,"psal":32.628,"temp":4.55},{"pres":57.6,"psal":32.634,"temp":4.557},{"pres":58.6,"psal":32.64,"temp":4.562},{"pres":59.6,"psal":32.641,"temp":4.569},{"pres":60.6,"psal":32.664,"temp":4.605},{"pres":61.6,"psal":32.684,"temp":4.636},{"pres":62.6,"psal":32.729,"temp":4.685},{"pres":63.5,"psal":32.772,"temp":4.719},{"pres":64.6,"psal":32.793,"temp":4.75},{"pres":65.6,"psal":32.833,"temp":4.788},{"pres":66.6,"psal":32.872,"temp":4.842},{"pres":67.6,"psal":32.921,"temp":4.894},{"pres":68.6,"psal":32.958,"temp":4.985},{"pres":69.6,"psal":33.052,"temp":5.058},{"pres":70.6,"psal":33.097,"temp":5.081},{"pres":71.6,"psal":33.137,"temp":5.121},{"pres":72.6,"psal":33.224,"temp":5.16},{"pres":73.6,"psal":33.267,"temp":5.187},{"pres":74.6,"psal":33.309,"temp":5.226},{"pres":75.6,"psal":33.379,"temp":5.258},{"pres":76.6,"psal":33.388,"temp":5.262},{"pres":77.6,"psal":33.408,"temp":5.287},{"pres":78.6,"psal":33.432,"temp":5.298},{"pres":79.6,"psal":33.436,"temp":5.299},{"pres":80.6,"psal":33.441,"temp":5.304},{"pres":81.7,"psal":33.46,"temp":5.308},{"pres":82.7,"psal":33.46,"temp":5.304},{"pres":83.7,"psal":33.464,"temp":5.308},{"pres":84.7,"psal":33.472,"temp":5.31},{"pres":85.7,"psal":33.48,"temp":5.285},{"pres":86.6,"psal":33.492,"temp":5.278},{"pres":87.6,"psal":33.519,"temp":5.287},{"pres":88.7,"psal":33.524,"temp":5.282},{"pres":89.7,"psal":33.531,"temp":5.289},{"pres":90.7,"psal":33.535,"temp":5.295},{"pres":91.6,"psal":33.542,"temp":5.297},{"pres":92.6,"psal":33.556,"temp":5.3},{"pres":93.6,"psal":33.583,"temp":5.299},{"pres":94.6,"psal":33.601,"temp":5.302},{"pres":95.6,"psal":33.612,"temp":5.301},{"pres":96.6,"psal":33.615,"temp":5.298},{"pres":97.6,"psal":33.615,"temp":5.299},{"pres":98.6,"psal":33.623,"temp":5.298},{"pres":99.7,"psal":33.634,"temp":5.295},{"pres":102.2,"psal":33.664,"temp":5.298},{"pres":104.7,"psal":33.681,"temp":5.297},{"pres":107.1,"psal":33.7,"temp":5.342},{"pres":109.6,"psal":33.734,"temp":5.351},{"pres":112.1,"psal":33.746,"temp":5.373},{"pres":114.7,"psal":33.762,"temp":5.424},{"pres":117.1,"psal":33.779,"temp":5.441},{"pres":119.6,"psal":33.783,"temp":5.437},{"pres":122.1,"psal":33.793,"temp":5.374},{"pres":124.6,"psal":33.791,"temp":5.329},{"pres":127.1,"psal":33.792,"temp":5.309},{"pres":129.6,"psal":33.791,"temp":5.297},{"pres":132.1,"psal":33.79,"temp":5.287},{"pres":134.5,"psal":33.792,"temp":5.242},{"pres":137,"psal":33.789,"temp":5.131},{"pres":139.6,"psal":33.78,"temp":5.081},{"pres":142.2,"psal":33.783,"temp":4.975},{"pres":144.6,"psal":33.777,"temp":4.899},{"pres":147.1,"psal":33.782,"temp":4.844},{"pres":149.6,"psal":33.78,"temp":4.822},{"pres":152.1,"psal":33.784,"temp":4.808},{"pres":154.6,"psal":33.786,"temp":4.775},{"pres":157.1,"psal":33.787,"temp":4.763},{"pres":159.6,"psal":33.794,"temp":4.75},{"pres":162,"psal":33.798,"temp":4.719},{"pres":164.6,"psal":33.797,"temp":4.697},{"pres":167.1,"psal":33.8,"temp":4.69},{"pres":169.6,"psal":33.802,"temp":4.685},{"pres":172.1,"psal":33.805,"temp":4.666},{"pres":174.6,"psal":33.804,"temp":4.634},{"pres":177.1,"psal":33.808,"temp":4.644},{"pres":179.5,"psal":33.814,"temp":4.652},{"pres":182.1,"psal":33.819,"temp":4.588},{"pres":184.6,"psal":33.816,"temp":4.535},{"pres":187.1,"psal":33.823,"temp":4.533},{"pres":189.6,"psal":33.826,"temp":4.506},{"pres":192.1,"psal":33.831,"temp":4.538},{"pres":194.6,"psal":33.839,"temp":4.555},{"pres":197.1,"psal":33.849,"temp":4.569},{"pres":199.6,"psal":33.851,"temp":4.573},{"pres":202.1,"psal":33.861,"temp":4.592},{"pres":204.6,"psal":33.865,"temp":4.603},{"pres":207.1,"psal":33.868,"temp":4.6},{"pres":209.6,"psal":33.869,"temp":4.597},{"pres":212.1,"psal":33.87,"temp":4.595},{"pres":214.6,"psal":33.872,"temp":4.592},{"pres":217.1,"psal":33.876,"temp":4.574},{"pres":219.6,"psal":33.877,"temp":4.563},{"pres":222.1,"psal":33.878,"temp":4.56},{"pres":224.7,"psal":33.878,"temp":4.559},{"pres":227.1,"psal":33.879,"temp":4.551},{"pres":229.6,"psal":33.886,"temp":4.518},{"pres":232.1,"psal":33.892,"temp":4.519},{"pres":234.7,"psal":33.899,"temp":4.517},{"pres":237.1,"psal":33.901,"temp":4.515},{"pres":239.6,"psal":33.904,"temp":4.523},{"pres":242.1,"psal":33.91,"temp":4.539},{"pres":244.6,"psal":33.916,"temp":4.544},{"pres":247.1,"psal":33.921,"temp":4.534},{"pres":249.6,"psal":33.924,"temp":4.503},{"pres":252.2,"psal":33.923,"temp":4.488},{"pres":254.6,"psal":33.925,"temp":4.47},{"pres":257.1,"psal":33.929,"temp":4.475},{"pres":259.6,"psal":33.934,"temp":4.474},{"pres":262,"psal":33.936,"temp":4.472},{"pres":264.6,"psal":33.937,"temp":4.467},{"pres":267.1,"psal":33.938,"temp":4.462},{"pres":269.6,"psal":33.938,"temp":4.458},{"pres":272.1,"psal":33.939,"temp":4.455},{"pres":274.7,"psal":33.942,"temp":4.448},{"pres":277.1,"psal":33.946,"temp":4.431},{"pres":279.5,"psal":33.945,"temp":4.42},{"pres":282.1,"psal":33.946,"temp":4.407},{"pres":284.6,"psal":33.946,"temp":4.401},{"pres":287.1,"psal":33.946,"temp":4.398},{"pres":289.6,"psal":33.947,"temp":4.391},{"pres":292.1,"psal":33.953,"temp":4.4},{"pres":294.6,"psal":33.956,"temp":4.398},{"pres":297.1,"psal":33.963,"temp":4.433},{"pres":299.6,"psal":33.969,"temp":4.436},{"pres":302.1,"psal":33.972,"temp":4.427},{"pres":304.6,"psal":33.975,"temp":4.418},{"pres":307.1,"psal":33.978,"temp":4.41},{"pres":309.6,"psal":33.98,"temp":4.405},{"pres":312.1,"psal":33.981,"temp":4.403},{"pres":314.5,"psal":33.982,"temp":4.401},{"pres":317.1,"psal":33.985,"temp":4.394},{"pres":319.6,"psal":33.987,"temp":4.388},{"pres":322.1,"psal":33.99,"temp":4.381},{"pres":324.6,"psal":33.992,"temp":4.376},{"pres":327.2,"psal":33.997,"temp":4.368},{"pres":329.7,"psal":34,"temp":4.361},{"pres":332.1,"psal":34.002,"temp":4.357},{"pres":334.6,"psal":34.004,"temp":4.354},{"pres":337.1,"psal":34.006,"temp":4.35},{"pres":339.6,"psal":34.007,"temp":4.348},{"pres":342.1,"psal":34.011,"temp":4.341},{"pres":344.7,"psal":34.014,"temp":4.334},{"pres":347.1,"psal":34.016,"temp":4.33},{"pres":349.6,"psal":34.02,"temp":4.322},{"pres":352.1,"psal":34.022,"temp":4.318},{"pres":354.6,"psal":34.025,"temp":4.309},{"pres":357.1,"psal":34.027,"temp":4.304},{"pres":359.6,"psal":34.029,"temp":4.295},{"pres":362.1,"psal":34.031,"temp":4.29},{"pres":364.6,"psal":34.034,"temp":4.282},{"pres":367.1,"psal":34.039,"temp":4.268},{"pres":369.6,"psal":34.041,"temp":4.261},{"pres":372.1,"psal":34.043,"temp":4.258},{"pres":374.6,"psal":34.047,"temp":4.25},{"pres":377.1,"psal":34.05,"temp":4.244},{"pres":379.6,"psal":34.054,"temp":4.237},{"pres":382.1,"psal":34.056,"temp":4.232},{"pres":384.6,"psal":34.058,"temp":4.225},{"pres":387.1,"psal":34.06,"temp":4.219},{"pres":389.6,"psal":34.061,"temp":4.212},{"pres":392.1,"psal":34.064,"temp":4.208},{"pres":394.6,"psal":34.065,"temp":4.205},{"pres":397.1,"psal":34.066,"temp":4.203},{"pres":399.6,"psal":34.068,"temp":4.201},{"pres":402.1,"psal":34.069,"temp":4.199},{"pres":404.6,"psal":34.072,"temp":4.199},{"pres":407.1,"psal":34.078,"temp":4.19},{"pres":409.5,"psal":34.079,"temp":4.182},{"pres":412.1,"psal":34.08,"temp":4.178},{"pres":414.6,"psal":34.082,"temp":4.173},{"pres":417.1,"psal":34.084,"temp":4.169},{"pres":419.7,"psal":34.087,"temp":4.16},{"pres":422.2,"psal":34.09,"temp":4.154},{"pres":424.6,"psal":34.092,"temp":4.148},{"pres":427.1,"psal":34.093,"temp":4.145},{"pres":429.6,"psal":34.096,"temp":4.137},{"pres":432,"psal":34.098,"temp":4.13},{"pres":434.6,"psal":34.098,"temp":4.128},{"pres":437.1,"psal":34.101,"temp":4.12},{"pres":439.6,"psal":34.103,"temp":4.113},{"pres":442.1,"psal":34.105,"temp":4.106},{"pres":444.6,"psal":34.107,"temp":4.1},{"pres":447.1,"psal":34.11,"temp":4.091},{"pres":449.6,"psal":34.113,"temp":4.084},{"pres":452.2,"psal":34.115,"temp":4.077},{"pres":454.6,"psal":34.118,"temp":4.068},{"pres":457.1,"psal":34.118,"temp":4.066},{"pres":459.5,"psal":34.119,"temp":4.06},{"pres":462.1,"psal":34.12,"temp":4.052},{"pres":464.6,"psal":34.119,"temp":4.039},{"pres":467.1,"psal":34.119,"temp":4.036},{"pres":469.6,"psal":34.122,"temp":4.026},{"pres":472.1,"psal":34.125,"temp":4.012},{"pres":474.6,"psal":34.126,"temp":4.003},{"pres":477.1,"psal":34.127,"temp":3.994},{"pres":479.6,"psal":34.128,"temp":3.986},{"pres":482.1,"psal":34.129,"temp":3.976},{"pres":484.6,"psal":34.132,"temp":3.959},{"pres":487.1,"psal":34.133,"temp":3.948},{"pres":489.6,"psal":34.134,"temp":3.942},{"pres":492.1,"psal":34.136,"temp":3.941},{"pres":494.7,"psal":34.141,"temp":3.941},{"pres":497.1,"psal":34.141,"temp":3.934},{"pres":499.6,"psal":34.145,"temp":3.943},{"pres":504.6,"psal":34.15,"temp":3.934},{"pres":509.6,"psal":34.154,"temp":3.918},{"pres":514.6,"psal":34.157,"temp":3.912},{"pres":519.6,"psal":34.159,"temp":3.912},{"pres":524.6,"psal":34.164,"temp":3.906},{"pres":529.6,"psal":34.167,"temp":3.89},{"pres":534.7,"psal":34.173,"temp":3.872},{"pres":539.7,"psal":34.175,"temp":3.864},{"pres":544.7,"psal":34.178,"temp":3.857},{"pres":549.6,"psal":34.182,"temp":3.845},{"pres":554.6,"psal":34.184,"temp":3.838},{"pres":559.6,"psal":34.187,"temp":3.828},{"pres":564.5,"psal":34.191,"temp":3.809},{"pres":569.6,"psal":34.193,"temp":3.797},{"pres":574.6,"psal":34.196,"temp":3.79},{"pres":579.6,"psal":34.201,"temp":3.774},{"pres":584.6,"psal":34.203,"temp":3.765},{"pres":589.6,"psal":34.205,"temp":3.762},{"pres":594.6,"psal":34.21,"temp":3.749},{"pres":599.6,"psal":34.213,"temp":3.731},{"pres":604.6,"psal":34.218,"temp":3.719},{"pres":609.6,"psal":34.223,"temp":3.725},{"pres":614.6,"psal":34.226,"temp":3.708},{"pres":619.6,"psal":34.226,"temp":3.696},{"pres":624.6,"psal":34.227,"temp":3.686},{"pres":629.6,"psal":34.228,"temp":3.677},{"pres":634.7,"psal":34.228,"temp":3.671},{"pres":639.6,"psal":34.229,"temp":3.668},{"pres":644.6,"psal":34.232,"temp":3.653},{"pres":649.6,"psal":34.234,"temp":3.633},{"pres":654.6,"psal":34.232,"temp":3.582},{"pres":659.6,"psal":34.23,"temp":3.552},{"pres":664.6,"psal":34.23,"temp":3.527},{"pres":669.6,"psal":34.234,"temp":3.515},{"pres":674.6,"psal":34.233,"temp":3.499},{"pres":679.6,"psal":34.233,"temp":3.483},{"pres":684.5,"psal":34.236,"temp":3.471},{"pres":689.5,"psal":34.238,"temp":3.472},{"pres":694.6,"psal":34.242,"temp":3.475},{"pres":699.7,"psal":34.247,"temp":3.472},{"pres":704.6,"psal":34.25,"temp":3.46},{"pres":709.6,"psal":34.25,"temp":3.45},{"pres":714.6,"psal":34.253,"temp":3.441},{"pres":719.6,"psal":34.256,"temp":3.429},{"pres":724.7,"psal":34.257,"temp":3.42},{"pres":729.6,"psal":34.259,"temp":3.412},{"pres":734.6,"psal":34.261,"temp":3.407},{"pres":739.6,"psal":34.264,"temp":3.395},{"pres":744.6,"psal":34.265,"temp":3.391},{"pres":749.6,"psal":34.266,"temp":3.386},{"pres":754.6,"psal":34.267,"temp":3.379},{"pres":759.6,"psal":34.268,"temp":3.37},{"pres":764.6,"psal":34.269,"temp":3.357},{"pres":769.6,"psal":34.27,"temp":3.35},{"pres":774.6,"psal":34.272,"temp":3.341},{"pres":779.6,"psal":34.272,"temp":3.333},{"pres":784.6,"psal":34.273,"temp":3.325},{"pres":789.6,"psal":34.277,"temp":3.309},{"pres":794.7,"psal":34.282,"temp":3.294},{"pres":799.6,"psal":34.284,"temp":3.288},{"pres":804.6,"psal":34.287,"temp":3.28},{"pres":809.6,"psal":34.288,"temp":3.277},{"pres":814.7,"psal":34.291,"temp":3.268},{"pres":819.6,"psal":34.294,"temp":3.252},{"pres":824.6,"psal":34.296,"temp":3.236},{"pres":829.6,"psal":34.296,"temp":3.23},{"pres":834.6,"psal":34.298,"temp":3.221},{"pres":839.6,"psal":34.3,"temp":3.213},{"pres":844.6,"psal":34.303,"temp":3.201},{"pres":849.6,"psal":34.306,"temp":3.193},{"pres":854.6,"psal":34.307,"temp":3.181},{"pres":859.6,"psal":34.308,"temp":3.174},{"pres":864.6,"psal":34.31,"temp":3.162},{"pres":869.6,"psal":34.315,"temp":3.145},{"pres":874.6,"psal":34.316,"temp":3.138},{"pres":879.6,"psal":34.317,"temp":3.136},{"pres":884.6,"psal":34.319,"temp":3.13},{"pres":889.6,"psal":34.32,"temp":3.126},{"pres":894.6,"psal":34.321,"temp":3.122},{"pres":899.6,"psal":34.324,"temp":3.112},{"pres":904.6,"psal":34.326,"temp":3.105},{"pres":909.6,"psal":34.328,"temp":3.094},{"pres":914.6,"psal":34.329,"temp":3.09},{"pres":919.6,"psal":34.331,"temp":3.083},{"pres":924.6,"psal":34.332,"temp":3.076},{"pres":929.6,"psal":34.333,"temp":3.074},{"pres":934.6,"psal":34.335,"temp":3.068},{"pres":939.6,"psal":34.336,"temp":3.059},{"pres":944.7,"psal":34.34,"temp":3.047},{"pres":949.6,"psal":34.341,"temp":3.039},{"pres":954.6,"psal":34.343,"temp":3.035},{"pres":959.6,"psal":34.346,"temp":3.025},{"pres":964.5,"psal":34.349,"temp":3.011},{"pres":969.5,"psal":34.351,"temp":2.999},{"pres":974.6,"psal":34.355,"temp":2.984},{"pres":979.6,"psal":34.357,"temp":2.976},{"pres":984.6,"psal":34.358,"temp":2.978},{"pres":989.6,"psal":34.36,"temp":2.971},{"pres":994.6,"psal":34.361,"temp":2.967},{"pres":999.7,"psal":34.364,"temp":2.959},{"pres":1004.6,"psal":34.365,"temp":2.954},{"pres":1009.6,"psal":34.366,"temp":2.948},{"pres":1014.6,"psal":34.369,"temp":2.936},{"pres":1019.7,"psal":34.37,"temp":2.928},{"pres":1024.6,"psal":34.372,"temp":2.924},{"pres":1029.6,"psal":34.374,"temp":2.914},{"pres":1034.5,"psal":34.375,"temp":2.908},{"pres":1039.6,"psal":34.377,"temp":2.897},{"pres":1044.6,"psal":34.379,"temp":2.888},{"pres":1049.6,"psal":34.381,"temp":2.879},{"pres":1054.6,"psal":34.381,"temp":2.874},{"pres":1059.6,"psal":34.383,"temp":2.866},{"pres":1064.6,"psal":34.385,"temp":2.856},{"pres":1069.7,"psal":34.387,"temp":2.844},{"pres":1074.6,"psal":34.389,"temp":2.838},{"pres":1079.6,"psal":34.392,"temp":2.825},{"pres":1084.7,"psal":34.394,"temp":2.815},{"pres":1089.6,"psal":34.396,"temp":2.81},{"pres":1094.6,"psal":34.397,"temp":2.809},{"pres":1099.6,"psal":34.4,"temp":2.798},{"pres":1104.6,"psal":34.4,"temp":2.793},{"pres":1109.6,"psal":34.401,"temp":2.787},{"pres":1114.6,"psal":34.403,"temp":2.778},{"pres":1119.6,"psal":34.405,"temp":2.772},{"pres":1124.6,"psal":34.406,"temp":2.77},{"pres":1129.6,"psal":34.406,"temp":2.766},{"pres":1134.6,"psal":34.408,"temp":2.761},{"pres":1139.7,"psal":34.41,"temp":2.751},{"pres":1144.6,"psal":34.411,"temp":2.746},{"pres":1149.7,"psal":34.413,"temp":2.735},{"pres":1154.6,"psal":34.414,"temp":2.73},{"pres":1159.6,"psal":34.415,"temp":2.725},{"pres":1164.6,"psal":34.415,"temp":2.724},{"pres":1169.6,"psal":34.416,"temp":2.722},{"pres":1174.6,"psal":34.417,"temp":2.718},{"pres":1179.7,"psal":34.417,"temp":2.717},{"pres":1184.6,"psal":34.418,"temp":2.714},{"pres":1189.6,"psal":34.419,"temp":2.708},{"pres":1194.5,"psal":34.42,"temp":2.702},{"pres":1199.5,"psal":34.421,"temp":2.697},{"pres":1204.6,"psal":34.422,"temp":2.693},{"pres":1209.7,"psal":34.424,"temp":2.686},{"pres":1214.6,"psal":34.426,"temp":2.674},{"pres":1219.6,"psal":34.426,"temp":2.672},{"pres":1224.6,"psal":34.428,"temp":2.665},{"pres":1229.6,"psal":34.43,"temp":2.653},{"pres":1234.6,"psal":34.431,"temp":2.649},{"pres":1239.6,"psal":34.433,"temp":2.64},{"pres":1244.6,"psal":34.434,"temp":2.634},{"pres":1249.6,"psal":34.436,"temp":2.628},{"pres":1254.7,"psal":34.436,"temp":2.627},{"pres":1259.6,"psal":34.437,"temp":2.622},{"pres":1264.6,"psal":34.438,"temp":2.616},{"pres":1269.6,"psal":34.439,"temp":2.61},{"pres":1274.6,"psal":34.44,"temp":2.606},{"pres":1279.6,"psal":34.442,"temp":2.601},{"pres":1284.6,"psal":34.443,"temp":2.597},{"pres":1289.6,"psal":34.443,"temp":2.593},{"pres":1294.6,"psal":34.444,"temp":2.588},{"pres":1299.6,"psal":34.446,"temp":2.579},{"pres":1304.6,"psal":34.448,"temp":2.57},{"pres":1309.6,"psal":34.449,"temp":2.564},{"pres":1314.6,"psal":34.45,"temp":2.56},{"pres":1319.6,"psal":34.451,"temp":2.556},{"pres":1324.6,"psal":34.452,"temp":2.551},{"pres":1329.7,"psal":34.454,"temp":2.543},{"pres":1334.6,"psal":34.455,"temp":2.539},{"pres":1339.6,"psal":34.455,"temp":2.535},{"pres":1344.5,"psal":34.456,"temp":2.534},{"pres":1349.5,"psal":34.456,"temp":2.532},{"pres":1354.6,"psal":34.457,"temp":2.526},{"pres":1359.6,"psal":34.458,"temp":2.521},{"pres":1364.6,"psal":34.46,"temp":2.512},{"pres":1369.6,"psal":34.461,"temp":2.508},{"pres":1374.5,"psal":34.462,"temp":2.501},{"pres":1379.5,"psal":34.463,"temp":2.497},{"pres":1384.6,"psal":34.465,"temp":2.489},{"pres":1389.7,"psal":34.465,"temp":2.484},{"pres":1394.6,"psal":34.465,"temp":2.482},{"pres":1399.6,"psal":34.468,"temp":2.47},{"pres":1404.6,"psal":34.47,"temp":2.463},{"pres":1409.6,"psal":34.471,"temp":2.458},{"pres":1414.6,"psal":34.471,"temp":2.456},{"pres":1419.6,"psal":34.472,"temp":2.451},{"pres":1424.7,"psal":34.474,"temp":2.443},{"pres":1429.6,"psal":34.476,"temp":2.432},{"pres":1434.6,"psal":34.477,"temp":2.426},{"pres":1439.7,"psal":34.479,"temp":2.416},{"pres":1444.6,"psal":34.481,"temp":2.41},{"pres":1449.5,"psal":34.481,"temp":2.408},{"pres":1454.5,"psal":34.481,"temp":2.408},{"pres":1459.6,"psal":34.482,"temp":2.405},{"pres":1464.6,"psal":34.484,"temp":2.395},{"pres":1469.6,"psal":34.484,"temp":2.392},{"pres":1474.6,"psal":34.485,"temp":2.387},{"pres":1479.6,"psal":34.487,"temp":2.38},{"pres":1484.6,"psal":34.489,"temp":2.37},{"pres":1489.6,"psal":34.49,"temp":2.365},{"pres":1494.6,"psal":34.491,"temp":2.361},{"pres":1499.6,"psal":34.492,"temp":2.357},{"pres":1504.6,"psal":34.493,"temp":2.35},{"pres":1509.6,"psal":34.495,"temp":2.342},{"pres":1514.5,"psal":34.496,"temp":2.338},{"pres":1519.6,"psal":34.496,"temp":2.335},{"pres":1524.6,"psal":34.498,"temp":2.329},{"pres":1529.6,"psal":34.499,"temp":2.323},{"pres":1534.6,"psal":34.501,"temp":2.315},{"pres":1539.6,"psal":34.502,"temp":2.308},{"pres":1544.6,"psal":34.502,"temp":2.308},{"pres":1549.5,"psal":34.503,"temp":2.306},{"pres":1554.5,"psal":34.504,"temp":2.302},{"pres":1559.5,"psal":34.505,"temp":2.296},{"pres":1564.6,"psal":34.506,"temp":2.289},{"pres":1569.6,"psal":34.507,"temp":2.285},{"pres":1574.6,"psal":34.508,"temp":2.281},{"pres":1579.7,"psal":34.509,"temp":2.278},{"pres":1584.6,"psal":34.509,"temp":2.275},{"pres":1589.6,"psal":34.51,"temp":2.272},{"pres":1594.6,"psal":34.511,"temp":2.269},{"pres":1599.6,"psal":34.511,"temp":2.267},{"pres":1604.6,"psal":34.512,"temp":2.265},{"pres":1609.6,"psal":34.512,"temp":2.261},{"pres":1614.6,"psal":34.513,"temp":2.258},{"pres":1619.6,"psal":34.514,"temp":2.255},{"pres":1624.6,"psal":34.514,"temp":2.252},{"pres":1629.6,"psal":34.516,"temp":2.247},{"pres":1634.6,"psal":34.516,"temp":2.243},{"pres":1639.6,"psal":34.518,"temp":2.236},{"pres":1644.6,"psal":34.519,"temp":2.231},{"pres":1649.6,"psal":34.52,"temp":2.227},{"pres":1654.6,"psal":34.521,"temp":2.219},{"pres":1659.5,"psal":34.522,"temp":2.215},{"pres":1664.6,"psal":34.524,"temp":2.205},{"pres":1669.7,"psal":34.526,"temp":2.196},{"pres":1674.6,"psal":34.527,"temp":2.192},{"pres":1679.6,"psal":34.528,"temp":2.187},{"pres":1684.6,"psal":34.529,"temp":2.182},{"pres":1689.7,"psal":34.53,"temp":2.175},{"pres":1694.6,"psal":34.531,"temp":2.17},{"pres":1699.6,"psal":34.533,"temp":2.162},{"pres":1704.6,"psal":34.534,"temp":2.154},{"pres":1709.5,"psal":34.536,"temp":2.146},{"pres":1714.6,"psal":34.537,"temp":2.14},{"pres":1719.6,"psal":34.539,"temp":2.133},{"pres":1724.7,"psal":34.54,"temp":2.128},{"pres":1729.7,"psal":34.541,"temp":2.119},{"pres":1734.6,"psal":34.542,"temp":2.114},{"pres":1739.6,"psal":34.544,"temp":2.107},{"pres":1744.6,"psal":34.544,"temp":2.104},{"pres":1749.6,"psal":34.546,"temp":2.097},{"pres":1754.6,"psal":34.547,"temp":2.089},{"pres":1759.5,"psal":34.548,"temp":2.083},{"pres":1764.5,"psal":34.551,"temp":2.073},{"pres":1769.5,"psal":34.552,"temp":2.065},{"pres":1774.6,"psal":34.553,"temp":2.063},{"pres":1779.6,"psal":34.553,"temp":2.062},{"pres":1784.6,"psal":34.553,"temp":2.06},{"pres":1789.7,"psal":34.554,"temp":2.055},{"pres":1794.7,"psal":34.555,"temp":2.051},{"pres":1799.6,"psal":34.556,"temp":2.048},{"pres":1804.6,"psal":34.556,"temp":2.045},{"pres":1809.7,"psal":34.557,"temp":2.041},{"pres":1814.6,"psal":34.557,"temp":2.039},{"pres":1819.6,"psal":34.559,"temp":2.034},{"pres":1824.6,"psal":34.56,"temp":2.027},{"pres":1829.6,"psal":34.56,"temp":2.024},{"pres":1834.6,"psal":34.561,"temp":2.02},{"pres":1839.6,"psal":34.562,"temp":2.015},{"pres":1844.5,"psal":34.563,"temp":2.012},{"pres":1849.5,"psal":34.564,"temp":2.008},{"pres":1854.6,"psal":34.564,"temp":2.006},{"pres":1859.6,"psal":34.566,"temp":1.999},{"pres":1864.6,"psal":34.567,"temp":1.994},{"pres":1869.7,"psal":34.568,"temp":1.985},{"pres":1874.6,"psal":34.57,"temp":1.979},{"pres":1879.6,"psal":34.57,"temp":1.974},{"pres":1884.6,"psal":34.571,"temp":1.969},{"pres":1889.6,"psal":34.572,"temp":1.965},{"pres":1894.6,"psal":34.574,"temp":1.957},{"pres":1899.6,"psal":34.575,"temp":1.952},{"pres":1904.5,"psal":34.576,"temp":1.947},{"pres":1909.4,"psal":34.576,"temp":1.944},{"pres":1914.6,"psal":34.577,"temp":1.943},{"pres":1919.6,"psal":34.577,"temp":1.939},{"pres":1924.6,"psal":34.578,"temp":1.937},{"pres":1929.6,"psal":34.579,"temp":1.933},{"pres":1934.7,"psal":34.579,"temp":1.932},{"pres":1939.6,"psal":34.58,"temp":1.928},{"pres":1944.6,"psal":34.58,"temp":1.925},{"pres":1949.5,"psal":34.581,"temp":1.923},{"pres":1954.5,"psal":34.581,"temp":1.922},{"pres":1959.5,"psal":34.582,"temp":1.916},{"pres":1964.6,"psal":34.583,"temp":1.91},{"pres":1969.6,"psal":34.584,"temp":1.909},{"pres":1974.6,"psal":34.584,"temp":1.908},{"pres":1979.6,"psal":34.584,"temp":1.905},{"pres":1984.6,"psal":34.585,"temp":1.9},{"pres":1989.6,"psal":34.586,"temp":1.897},{"pres":1994.6,"psal":34.586,"temp":1.895},{"pres":1999.6,"psal":34.587,"temp":1.894},{"pres":2004.6,"psal":34.587,"temp":1.892},{"pres":2009.6,"psal":34.588,"temp":1.89},{"pres":2014.6,"psal":34.588,"temp":1.887},{"pres":2019.6,"psal":34.588,"temp":1.886}],"station_parameters":["pres","psal","temp"],"pres_max_for_TEMP":2019.6,"pres_min_for_TEMP":1.7,"pres_max_for_PSAL":2019.6,"pres_min_for_PSAL":1.7,"max_pres":2019.6,"date":"2023-01-26T05:55:00.000Z","date_added":"2023-01-27T08:08:38.067Z","date_qc":1,"lat":55.615684509277344,"lon":-153.22265625,"geoLocation":{"type":"Point","coordinates":[-153.22265625,55.615684509277344]},"position_qc":1,"cycle_number":166,"dac":"meds","platform_number":4902461,"station_parameters_in_nc":["MTIME","PRES","PSAL","TEMP"],"nc_url":"ftp://ftp.ifremer.fr/ifremer/argo/dac/meds/4902461/profiles/R4902461_166.nc","DIRECTION":"A","BASIN":2,"core_data_mode":"A","roundLat":"55.616","roundLon":"-153.223","strLat":"55.616 N","strLon":"153.223 W","formatted_station_parameters":[" pres"," psal"," temp"]},{"_id":"4902521_56","POSITIONING_SYSTEM":"GPS","DATA_CENTRE":"ME","PI_NAME":"Blair Greenan","WMO_INST_TYPE":"844","VERTICAL_SAMPLING_SCHEME":"Primary sampling: averaged","DATA_MODE":"R","PLATFORM_TYPE":"ARVOR","measurements":[{"pres":3.4,"psal":32.497,"temp":4.854},{"pres":4,"psal":32.498,"temp":4.852},{"pres":5.1,"psal":32.498,"temp":4.852},{"pres":6,"psal":32.498,"temp":4.853},{"pres":7.1,"psal":32.498,"temp":4.854},{"pres":7.9,"psal":32.498,"temp":4.855},{"pres":9,"psal":32.498,"temp":4.855},{"pres":10.1,"psal":32.497,"temp":4.856},{"pres":11.1,"psal":32.498,"temp":4.857},{"pres":12.1,"psal":32.498,"temp":4.855},{"pres":13.1,"psal":32.498,"temp":4.854},{"pres":14,"psal":32.498,"temp":4.855},{"pres":15.2,"psal":32.498,"temp":4.855},{"pres":16.1,"psal":32.498,"temp":4.855},{"pres":16.8,"psal":32.498,"temp":4.854},{"pres":17.8,"psal":32.498,"temp":4.858},{"pres":18.8,"psal":32.499,"temp":4.858},{"pres":19.9,"psal":32.498,"temp":4.858},{"pres":21,"psal":32.498,"temp":4.859},{"pres":22.1,"psal":32.498,"temp":4.859},{"pres":22.9,"psal":32.499,"temp":4.859},{"pres":23.7,"psal":32.498,"temp":4.859},{"pres":24.8,"psal":32.498,"temp":4.859},{"pres":25.9,"psal":32.498,"temp":4.859},{"pres":26.9,"psal":32.498,"temp":4.859},{"pres":27.9,"psal":32.499,"temp":4.859},{"pres":29.1,"psal":32.5,"temp":4.858},{"pres":30.1,"psal":32.499,"temp":4.858},{"pres":31.1,"psal":32.499,"temp":4.858},{"pres":32.1,"psal":32.499,"temp":4.859},{"pres":33.1,"psal":32.499,"temp":4.858},{"pres":34.1,"psal":32.5,"temp":4.857},{"pres":35.1,"psal":32.5,"temp":4.856},{"pres":35.9,"psal":32.501,"temp":4.856},{"pres":37,"psal":32.502,"temp":4.854},{"pres":38,"psal":32.502,"temp":4.855},{"pres":39,"psal":32.502,"temp":4.854},{"pres":40,"psal":32.502,"temp":4.854},{"pres":41.1,"psal":32.502,"temp":4.853},{"pres":42.1,"psal":32.503,"temp":4.853},{"pres":43,"psal":32.504,"temp":4.853},{"pres":44.1,"psal":32.504,"temp":4.852},{"pres":45,"psal":32.505,"temp":4.852},{"pres":45.9,"psal":32.505,"temp":4.852},{"pres":46.8,"psal":32.505,"temp":4.852},{"pres":47.8,"psal":32.507,"temp":4.85},{"pres":48.8,"psal":32.508,"temp":4.849},{"pres":49.8,"psal":32.507,"temp":4.849},{"pres":50.8,"psal":32.507,"temp":4.849},{"pres":51.8,"psal":32.508,"temp":4.849},{"pres":52.9,"psal":32.509,"temp":4.846},{"pres":53.9,"psal":32.509,"temp":4.846},{"pres":54.9,"psal":32.51,"temp":4.845},{"pres":56,"psal":32.511,"temp":4.844},{"pres":57,"psal":32.511,"temp":4.842},{"pres":58,"psal":32.512,"temp":4.839},{"pres":58.9,"psal":32.514,"temp":4.835},{"pres":60.1,"psal":32.515,"temp":4.833},{"pres":60.9,"psal":32.517,"temp":4.83},{"pres":62,"psal":32.518,"temp":4.825},{"pres":63,"psal":32.52,"temp":4.822},{"pres":64,"psal":32.521,"temp":4.819},{"pres":64.8,"psal":32.522,"temp":4.817},{"pres":66,"psal":32.525,"temp":4.809},{"pres":67.1,"psal":32.534,"temp":4.778},{"pres":68,"psal":32.544,"temp":4.739},{"pres":69,"psal":32.547,"temp":4.731},{"pres":69.9,"psal":32.548,"temp":4.729},{"pres":70.9,"psal":32.549,"temp":4.728},{"pres":71.9,"psal":32.55,"temp":4.712},{"pres":73.1,"psal":32.554,"temp":4.706},{"pres":74,"psal":32.558,"temp":4.692},{"pres":75,"psal":32.561,"temp":4.683},{"pres":76,"psal":32.566,"temp":4.671},{"pres":77.1,"psal":32.567,"temp":4.657},{"pres":78.1,"psal":32.571,"temp":4.621},{"pres":79,"psal":32.574,"temp":4.597},{"pres":79.8,"psal":32.576,"temp":4.581},{"pres":80.8,"psal":32.579,"temp":4.579},{"pres":81.8,"psal":32.592,"temp":4.627},{"pres":82.8,"psal":32.607,"temp":4.688},{"pres":83.7,"psal":32.636,"temp":4.737},{"pres":84.7,"psal":32.654,"temp":4.739},{"pres":86.1,"psal":32.663,"temp":4.733},{"pres":87.1,"psal":32.684,"temp":4.713},{"pres":88.1,"psal":32.698,"temp":4.718},{"pres":89.1,"psal":32.706,"temp":4.726},{"pres":90.1,"psal":32.789,"temp":4.756},{"pres":91,"psal":32.814,"temp":4.737},{"pres":91.8,"psal":32.893,"temp":4.696},{"pres":92.8,"psal":33.018,"temp":4.678},{"pres":93.8,"psal":33.176,"temp":4.611},{"pres":95,"psal":33.378,"temp":4.474},{"pres":95.9,"psal":33.404,"temp":4.461},{"pres":96.9,"psal":33.423,"temp":4.448},{"pres":97.9,"psal":33.436,"temp":4.44},{"pres":98.9,"psal":33.445,"temp":4.433},{"pres":99.9,"psal":33.447,"temp":4.431},{"pres":100.9,"psal":33.454,"temp":4.426},{"pres":102,"psal":33.46,"temp":4.418},{"pres":102.9,"psal":33.461,"temp":4.416},{"pres":104,"psal":33.46,"temp":4.414},{"pres":105,"psal":33.486,"temp":4.4},{"pres":106.1,"psal":33.496,"temp":4.399},{"pres":106.8,"psal":33.502,"temp":4.396},{"pres":107.9,"psal":33.523,"temp":4.387},{"pres":109.1,"psal":33.532,"temp":4.385},{"pres":109.9,"psal":33.539,"temp":4.382},{"pres":110.8,"psal":33.547,"temp":4.379},{"pres":111.6,"psal":33.553,"temp":4.376},{"pres":112.9,"psal":33.561,"temp":4.374},{"pres":114.2,"psal":33.57,"temp":4.374},{"pres":115.1,"psal":33.571,"temp":4.375},{"pres":116,"psal":33.572,"temp":4.375},{"pres":116.8,"psal":33.581,"temp":4.373},{"pres":117.7,"psal":33.587,"temp":4.371},{"pres":118.6,"psal":33.588,"temp":4.37},{"pres":119.9,"psal":33.601,"temp":4.369},{"pres":121.3,"psal":33.625,"temp":4.373},{"pres":122.2,"psal":33.631,"temp":4.371},{"pres":123.1,"psal":33.639,"temp":4.366},{"pres":124,"psal":33.658,"temp":4.347},{"pres":125,"psal":33.661,"temp":4.34},{"pres":125.9,"psal":33.681,"temp":4.332},{"pres":126.9,"psal":33.697,"temp":4.323},{"pres":127.9,"psal":33.697,"temp":4.322},{"pres":128.9,"psal":33.702,"temp":4.32},{"pres":129.9,"psal":33.709,"temp":4.319},{"pres":130.9,"psal":33.717,"temp":4.323},{"pres":131.9,"psal":33.724,"temp":4.321},{"pres":132.9,"psal":33.746,"temp":4.329},{"pres":133.9,"psal":33.748,"temp":4.33},{"pres":135,"psal":33.756,"temp":4.325},{"pres":136,"psal":33.764,"temp":4.314},{"pres":137,"psal":33.774,"temp":4.311},{"pres":138.1,"psal":33.778,"temp":4.31},{"pres":139.1,"psal":33.78,"temp":4.308},{"pres":140.1,"psal":33.781,"temp":4.307},{"pres":141.1,"psal":33.786,"temp":4.301},{"pres":142.1,"psal":33.787,"temp":4.3},{"pres":143.1,"psal":33.792,"temp":4.295},{"pres":144.1,"psal":33.795,"temp":4.291},{"pres":145.1,"psal":33.812,"temp":4.27},{"pres":146.1,"psal":33.82,"temp":4.257},{"pres":147.1,"psal":33.822,"temp":4.254},{"pres":148.1,"psal":33.825,"temp":4.25},{"pres":149.1,"psal":33.828,"temp":4.247},{"pres":150.2,"psal":33.828,"temp":4.245},{"pres":151.2,"psal":33.829,"temp":4.243},{"pres":152.2,"psal":33.83,"temp":4.241},{"pres":153.2,"psal":33.833,"temp":4.236},{"pres":154.1,"psal":33.833,"temp":4.234},{"pres":155.1,"psal":33.836,"temp":4.228},{"pres":156.1,"psal":33.84,"temp":4.221},{"pres":157.1,"psal":33.844,"temp":4.211},{"pres":158.1,"psal":33.846,"temp":4.207},{"pres":159,"psal":33.848,"temp":4.204},{"pres":160,"psal":33.848,"temp":4.203},{"pres":160.9,"psal":33.851,"temp":4.197},{"pres":161.9,"psal":33.854,"temp":4.192},{"pres":162.8,"psal":33.854,"temp":4.19},{"pres":163.8,"psal":33.856,"temp":4.186},{"pres":164.7,"psal":33.856,"temp":4.184},{"pres":165.7,"psal":33.858,"temp":4.182},{"pres":166.6,"psal":33.858,"temp":4.182},{"pres":167.6,"psal":33.859,"temp":4.179},{"pres":168.5,"psal":33.86,"temp":4.176},{"pres":169.5,"psal":33.862,"temp":4.173},{"pres":170.9,"psal":33.864,"temp":4.168},{"pres":172.4,"psal":33.868,"temp":4.162},{"pres":173.3,"psal":33.871,"temp":4.151},{"pres":174.3,"psal":33.874,"temp":4.147},{"pres":175.2,"psal":33.877,"temp":4.142},{"pres":176.2,"psal":33.88,"temp":4.138},{"pres":177.1,"psal":33.881,"temp":4.136},{"pres":178.1,"psal":33.882,"temp":4.134},{"pres":179.1,"psal":33.883,"temp":4.132},{"pres":180,"psal":33.883,"temp":4.131},{"pres":180.9,"psal":33.884,"temp":4.131},{"pres":181.8,"psal":33.885,"temp":4.128},{"pres":182.9,"psal":33.889,"temp":4.125},{"pres":184,"psal":33.891,"temp":4.124},{"pres":184.7,"psal":33.892,"temp":4.122},{"pres":185.8,"psal":33.893,"temp":4.12},{"pres":187,"psal":33.895,"temp":4.118},{"pres":187.8,"psal":33.896,"temp":4.117},{"pres":188.9,"psal":33.899,"temp":4.115},{"pres":190.1,"psal":33.902,"temp":4.111},{"pres":190.9,"psal":33.906,"temp":4.107},{"pres":191.7,"psal":33.906,"temp":4.108},{"pres":192.9,"psal":33.908,"temp":4.105},{"pres":194.1,"psal":33.909,"temp":4.104},{"pres":194.9,"psal":33.909,"temp":4.103},{"pres":195.7,"psal":33.911,"temp":4.101},{"pres":197,"psal":33.913,"temp":4.098},{"pres":198.2,"psal":33.918,"temp":4.093},{"pres":199,"psal":33.919,"temp":4.091},{"pres":199.9,"psal":33.921,"temp":4.089},{"pres":200.7,"psal":33.921,"temp":4.089},{"pres":202,"psal":33.922,"temp":4.086},{"pres":203.3,"psal":33.924,"temp":4.086},{"pres":204.2,"psal":33.926,"temp":4.083},{"pres":205.1,"psal":33.928,"temp":4.081},{"pres":206,"psal":33.929,"temp":4.08},{"pres":206.8,"psal":33.929,"temp":4.08},{"pres":207.7,"psal":33.929,"temp":4.08},{"pres":208.6,"psal":33.931,"temp":4.078},{"pres":209.6,"psal":33.932,"temp":4.076},{"pres":210.9,"psal":33.932,"temp":4.075},{"pres":212.3,"psal":33.933,"temp":4.074},{"pres":213.2,"psal":33.934,"temp":4.074},{"pres":214.2,"psal":33.934,"temp":4.073},{"pres":215.1,"psal":33.934,"temp":4.073},{"pres":216.1,"psal":33.934,"temp":4.072},{"pres":217,"psal":33.935,"temp":4.071},{"pres":218,"psal":33.937,"temp":4.07},{"pres":218.9,"psal":33.938,"temp":4.068},{"pres":219.9,"psal":33.937,"temp":4.068},{"pres":220.9,"psal":33.938,"temp":4.068},{"pres":221.8,"psal":33.937,"temp":4.068},{"pres":222.8,"psal":33.939,"temp":4.066},{"pres":223.8,"psal":33.94,"temp":4.064},{"pres":224.8,"psal":33.941,"temp":4.063},{"pres":225.8,"psal":33.941,"temp":4.062},{"pres":226.8,"psal":33.942,"temp":4.061},{"pres":227.8,"psal":33.945,"temp":4.058},{"pres":228.8,"psal":33.946,"temp":4.058},{"pres":229.8,"psal":33.947,"temp":4.057},{"pres":230.8,"psal":33.948,"temp":4.057},{"pres":231.8,"psal":33.949,"temp":4.057},{"pres":232.8,"psal":33.95,"temp":4.056},{"pres":233.8,"psal":33.95,"temp":4.055},{"pres":234.8,"psal":33.951,"temp":4.055},{"pres":235.8,"psal":33.951,"temp":4.054},{"pres":236.8,"psal":33.952,"temp":4.053},{"pres":237.8,"psal":33.952,"temp":4.053},{"pres":238.9,"psal":33.952,"temp":4.053},{"pres":239.9,"psal":33.953,"temp":4.052},{"pres":240.9,"psal":33.953,"temp":4.052},{"pres":241.9,"psal":33.956,"temp":4.049},{"pres":242.9,"psal":33.957,"temp":4.048},{"pres":244,"psal":33.957,"temp":4.048},{"pres":245,"psal":33.958,"temp":4.047},{"pres":246,"psal":33.959,"temp":4.046},{"pres":247,"psal":33.959,"temp":4.046},{"pres":248,"psal":33.96,"temp":4.046},{"pres":249,"psal":33.96,"temp":4.045},{"pres":250.1,"psal":33.961,"temp":4.044},{"pres":251.1,"psal":33.962,"temp":4.043},{"pres":252.1,"psal":33.962,"temp":4.043},{"pres":253.1,"psal":33.963,"temp":4.042},{"pres":254.1,"psal":33.964,"temp":4.04},{"pres":255,"psal":33.965,"temp":4.039},{"pres":256,"psal":33.966,"temp":4.039},{"pres":257,"psal":33.966,"temp":4.038},{"pres":258,"psal":33.967,"temp":4.038},{"pres":259,"psal":33.966,"temp":4.038},{"pres":260,"psal":33.967,"temp":4.037},{"pres":261,"psal":33.968,"temp":4.036},{"pres":262,"psal":33.969,"temp":4.034},{"pres":263,"psal":33.97,"temp":4.033},{"pres":264,"psal":33.971,"temp":4.031},{"pres":265,"psal":33.974,"temp":4.029},{"pres":266,"psal":33.975,"temp":4.028},{"pres":267,"psal":33.979,"temp":4.024},{"pres":267.9,"psal":33.978,"temp":4.025},{"pres":268.9,"psal":33.979,"temp":4.024},{"pres":269.9,"psal":33.979,"temp":4.024},{"pres":270.8,"psal":33.979,"temp":4.024},{"pres":271.8,"psal":33.981,"temp":4.022},{"pres":272.8,"psal":33.982,"temp":4.021},{"pres":273.7,"psal":33.982,"temp":4.02},{"pres":274.7,"psal":33.983,"temp":4.02},{"pres":275.7,"psal":33.983,"temp":4.019},{"pres":276.7,"psal":33.983,"temp":4.019},{"pres":277.6,"psal":33.984,"temp":4.018},{"pres":278.6,"psal":33.984,"temp":4.018},{"pres":279.5,"psal":33.985,"temp":4.018},{"pres":280.5,"psal":33.984,"temp":4.018},{"pres":281.9,"psal":33.985,"temp":4.017},{"pres":283.4,"psal":33.986,"temp":4.016},{"pres":284.4,"psal":33.986,"temp":4.015},{"pres":285.3,"psal":33.987,"temp":4.014},{"pres":286.3,"psal":33.989,"temp":4.013},{"pres":287.2,"psal":33.99,"temp":4.011},{"pres":288.2,"psal":33.991,"temp":4.01},{"pres":289.1,"psal":33.992,"temp":4.009},{"pres":290,"psal":33.992,"temp":4.01},{"pres":291,"psal":33.994,"temp":4.008},{"pres":291.9,"psal":33.994,"temp":4.008},{"pres":292.9,"psal":33.995,"temp":4.006},{"pres":293.8,"psal":33.997,"temp":4.004},{"pres":294.8,"psal":33.999,"temp":3.997},{"pres":295.7,"psal":34.002,"temp":3.995},{"pres":296.7,"psal":34.002,"temp":3.994},{"pres":297.6,"psal":34.003,"temp":3.994},{"pres":298.6,"psal":34.003,"temp":3.993},{"pres":299.5,"psal":34.005,"temp":3.991},{"pres":300.9,"psal":34.009,"temp":3.987},{"pres":302.4,"psal":34.012,"temp":3.984},{"pres":303.3,"psal":34.011,"temp":3.984},{"pres":304.2,"psal":34.012,"temp":3.984},{"pres":305.2,"psal":34.012,"temp":3.984},{"pres":306.1,"psal":34.012,"temp":3.984},{"pres":307.1,"psal":34.012,"temp":3.984},{"pres":308,"psal":34.014,"temp":3.982},{"pres":309,"psal":34.014,"temp":3.981},{"pres":310,"psal":34.015,"temp":3.981},{"pres":310.9,"psal":34.015,"temp":3.981},{"pres":311.9,"psal":34.016,"temp":3.979},{"pres":312.8,"psal":34.018,"temp":3.979},{"pres":313.8,"psal":34.018,"temp":3.978},{"pres":314.8,"psal":34.018,"temp":3.978},{"pres":315.7,"psal":34.02,"temp":3.975},{"pres":316.7,"psal":34.024,"temp":3.969},{"pres":317.7,"psal":34.024,"temp":3.969},{"pres":318.7,"psal":34.024,"temp":3.969},{"pres":319.6,"psal":34.025,"temp":3.968},{"pres":320.6,"psal":34.025,"temp":3.968},{"pres":321.5,"psal":34.026,"temp":3.967},{"pres":322.5,"psal":34.026,"temp":3.966},{"pres":323.5,"psal":34.028,"temp":3.964},{"pres":324.5,"psal":34.029,"temp":3.962},{"pres":325.9,"psal":34.035,"temp":3.954},{"pres":327.4,"psal":34.036,"temp":3.952},{"pres":328.4,"psal":34.036,"temp":3.952},{"pres":329.4,"psal":34.037,"temp":3.952},{"pres":330.4,"psal":34.038,"temp":3.951},{"pres":331.4,"psal":34.04,"temp":3.949},{"pres":332.3,"psal":34.04,"temp":3.949},{"pres":333.4,"psal":34.04,"temp":3.948},{"pres":334.3,"psal":34.043,"temp":3.945},{"pres":335.4,"psal":34.044,"temp":3.943},{"pres":336.4,"psal":34.044,"temp":3.942},{"pres":337.4,"psal":34.046,"temp":3.94},{"pres":338.4,"psal":34.047,"temp":3.938},{"pres":339.4,"psal":34.048,"temp":3.937},{"pres":340.4,"psal":34.048,"temp":3.937},{"pres":341.4,"psal":34.05,"temp":3.935},{"pres":342.5,"psal":34.05,"temp":3.935},{"pres":343.5,"psal":34.051,"temp":3.934},{"pres":344.5,"psal":34.054,"temp":3.931},{"pres":345.6,"psal":34.055,"temp":3.93},{"pres":346.6,"psal":34.058,"temp":3.926},{"pres":347.6,"psal":34.06,"temp":3.922},{"pres":348.7,"psal":34.06,"temp":3.922},{"pres":349.7,"psal":34.062,"temp":3.92},{"pres":350.7,"psal":34.062,"temp":3.92},{"pres":351.6,"psal":34.066,"temp":3.916},{"pres":353,"psal":34.066,"temp":3.913},{"pres":354.2,"psal":34.066,"temp":3.913},{"pres":355,"psal":34.067,"temp":3.913},{"pres":355.8,"psal":34.066,"temp":3.913},{"pres":357,"psal":34.066,"temp":3.912},{"pres":358.1,"psal":34.067,"temp":3.912},{"pres":358.9,"psal":34.067,"temp":3.911},{"pres":359.7,"psal":34.066,"temp":3.912},{"pres":360.9,"psal":34.07,"temp":3.904},{"pres":362.1,"psal":34.072,"temp":3.9},{"pres":362.9,"psal":34.072,"temp":3.899},{"pres":363.7,"psal":34.073,"temp":3.898},{"pres":365,"psal":34.073,"temp":3.897},{"pres":366.2,"psal":34.073,"temp":3.897},{"pres":367,"psal":34.076,"temp":3.893},{"pres":367.8,"psal":34.076,"temp":3.893},{"pres":368.6,"psal":34.077,"temp":3.892},{"pres":369.9,"psal":34.076,"temp":3.891},{"pres":371.1,"psal":34.077,"temp":3.892},{"pres":371.9,"psal":34.078,"temp":3.891},{"pres":372.7,"psal":34.078,"temp":3.891},{"pres":373.9,"psal":34.078,"temp":3.89},{"pres":375.2,"psal":34.08,"temp":3.889},{"pres":376,"psal":34.082,"temp":3.887},{"pres":376.9,"psal":34.082,"temp":3.887},{"pres":377.7,"psal":34.082,"temp":3.887},{"pres":378.9,"psal":34.082,"temp":3.887},{"pres":380.2,"psal":34.083,"temp":3.888},{"pres":381,"psal":34.083,"temp":3.888},{"pres":381.8,"psal":34.085,"temp":3.886},{"pres":382.7,"psal":34.086,"temp":3.884},{"pres":383.9,"psal":34.09,"temp":3.875},{"pres":385.2,"psal":34.094,"temp":3.869},{"pres":386,"psal":34.095,"temp":3.866},{"pres":386.8,"psal":34.097,"temp":3.864},{"pres":387.7,"psal":34.097,"temp":3.863},{"pres":388.9,"psal":34.098,"temp":3.861},{"pres":390.2,"psal":34.1,"temp":3.857},{"pres":391,"psal":34.103,"temp":3.85},{"pres":391.9,"psal":34.104,"temp":3.846},{"pres":392.7,"psal":34.104,"temp":3.845},{"pres":393.9,"psal":34.104,"temp":3.845},{"pres":395.2,"psal":34.105,"temp":3.844},{"pres":396.1,"psal":34.105,"temp":3.844},{"pres":396.9,"psal":34.105,"temp":3.843},{"pres":397.7,"psal":34.105,"temp":3.843},{"pres":399,"psal":34.105,"temp":3.838},{"pres":400.2,"psal":34.108,"temp":3.831},{"pres":401.1,"psal":34.11,"temp":3.831},{"pres":401.9,"psal":34.111,"temp":3.83},{"pres":402.8,"psal":34.111,"temp":3.83},{"pres":403.6,"psal":34.111,"temp":3.83},{"pres":404.9,"psal":34.111,"temp":3.829},{"pres":406.2,"psal":34.115,"temp":3.826},{"pres":407.1,"psal":34.116,"temp":3.823},{"pres":407.9,"psal":34.116,"temp":3.821},{"pres":408.8,"psal":34.117,"temp":3.819},{"pres":409.7,"psal":34.118,"temp":3.817},{"pres":410.9,"psal":34.118,"temp":3.817},{"pres":412.2,"psal":34.119,"temp":3.815},{"pres":413.1,"psal":34.119,"temp":3.815},{"pres":413.9,"psal":34.12,"temp":3.814},{"pres":414.8,"psal":34.12,"temp":3.814},{"pres":415.6,"psal":34.121,"temp":3.813},{"pres":416.9,"psal":34.122,"temp":3.81},{"pres":418.2,"psal":34.123,"temp":3.809},{"pres":419.1,"psal":34.123,"temp":3.808},{"pres":419.9,"psal":34.123,"temp":3.808},{"pres":420.8,"psal":34.124,"temp":3.806},{"pres":421.6,"psal":34.124,"temp":3.807},{"pres":422.9,"psal":34.126,"temp":3.804},{"pres":424.2,"psal":34.127,"temp":3.801},{"pres":425.1,"psal":34.127,"temp":3.801},{"pres":425.9,"psal":34.127,"temp":3.801},{"pres":426.7,"psal":34.128,"temp":3.8},{"pres":427.6,"psal":34.128,"temp":3.8},{"pres":428.9,"psal":34.128,"temp":3.798},{"pres":430.2,"psal":34.129,"temp":3.793},{"pres":431.1,"psal":34.129,"temp":3.792},{"pres":431.9,"psal":34.13,"temp":3.791},{"pres":432.8,"psal":34.131,"temp":3.79},{"pres":433.7,"psal":34.131,"temp":3.79},{"pres":434.9,"psal":34.131,"temp":3.786},{"pres":436.3,"psal":34.133,"temp":3.785},{"pres":437.2,"psal":34.132,"temp":3.785},{"pres":438.1,"psal":34.133,"temp":3.785},{"pres":438.9,"psal":34.133,"temp":3.785},{"pres":439.8,"psal":34.135,"temp":3.781},{"pres":440.7,"psal":34.135,"temp":3.781},{"pres":441.6,"psal":34.135,"temp":3.78},{"pres":442.9,"psal":34.136,"temp":3.776},{"pres":444.2,"psal":34.138,"temp":3.772},{"pres":445.1,"psal":34.139,"temp":3.77},{"pres":446,"psal":34.139,"temp":3.768},{"pres":446.9,"psal":34.14,"temp":3.766},{"pres":447.8,"psal":34.14,"temp":3.765},{"pres":448.7,"psal":34.141,"temp":3.763},{"pres":449.6,"psal":34.141,"temp":3.76},{"pres":450.9,"psal":34.142,"temp":3.751},{"pres":452.3,"psal":34.144,"temp":3.749},{"pres":453.2,"psal":34.143,"temp":3.748},{"pres":454.1,"psal":34.144,"temp":3.747},{"pres":455,"psal":34.144,"temp":3.747},{"pres":455.9,"psal":34.144,"temp":3.747},{"pres":456.8,"psal":34.144,"temp":3.746},{"pres":457.7,"psal":34.145,"temp":3.744},{"pres":458.6,"psal":34.146,"temp":3.741},{"pres":459.9,"psal":34.149,"temp":3.734},{"pres":461.3,"psal":34.15,"temp":3.731},{"pres":462.2,"psal":34.151,"temp":3.73},{"pres":463.1,"psal":34.151,"temp":3.728},{"pres":464,"psal":34.152,"temp":3.727},{"pres":464.9,"psal":34.153,"temp":3.725},{"pres":465.8,"psal":34.153,"temp":3.723},{"pres":466.7,"psal":34.153,"temp":3.722},{"pres":467.6,"psal":34.154,"temp":3.721},{"pres":468.9,"psal":34.155,"temp":3.72},{"pres":470.2,"psal":34.156,"temp":3.718},{"pres":471.2,"psal":34.157,"temp":3.715},{"pres":472.1,"psal":34.157,"temp":3.713},{"pres":472.9,"psal":34.158,"temp":3.711},{"pres":473.8,"psal":34.159,"temp":3.71},{"pres":474.8,"psal":34.159,"temp":3.709},{"pres":477.9,"psal":34.162,"temp":3.7},{"pres":482.9,"psal":34.166,"temp":3.69},{"pres":488,"psal":34.169,"temp":3.681},{"pres":493.3,"psal":34.173,"temp":3.665},{"pres":498.1,"psal":34.179,"temp":3.646},{"pres":503.1,"psal":34.179,"temp":3.644},{"pres":508.1,"psal":34.182,"temp":3.633},{"pres":513.2,"psal":34.188,"temp":3.618},{"pres":517.9,"psal":34.192,"temp":3.606},{"pres":522.8,"psal":34.196,"temp":3.602},{"pres":527.8,"psal":34.198,"temp":3.594},{"pres":532.8,"psal":34.201,"temp":3.585},{"pres":537.8,"psal":34.202,"temp":3.582},{"pres":543,"psal":34.203,"temp":3.579},{"pres":547.8,"psal":34.205,"temp":3.571},{"pres":552.7,"psal":34.207,"temp":3.564},{"pres":558.1,"psal":34.209,"temp":3.559},{"pres":563,"psal":34.211,"temp":3.556},{"pres":567.9,"psal":34.214,"temp":3.545},{"pres":573,"psal":34.215,"temp":3.54},{"pres":577.7,"psal":34.219,"temp":3.527},{"pres":582.9,"psal":34.222,"temp":3.517},{"pres":588.2,"psal":34.226,"temp":3.503},{"pres":592.9,"psal":34.23,"temp":3.488},{"pres":597.7,"psal":34.231,"temp":3.483},{"pres":603,"psal":34.234,"temp":3.474},{"pres":608.3,"psal":34.235,"temp":3.467},{"pres":613.2,"psal":34.236,"temp":3.46},{"pres":618.2,"psal":34.239,"temp":3.453},{"pres":623.2,"psal":34.241,"temp":3.444},{"pres":628.2,"psal":34.244,"temp":3.435},{"pres":633.3,"psal":34.247,"temp":3.425},{"pres":638.4,"psal":34.251,"temp":3.415},{"pres":643.4,"psal":34.253,"temp":3.406},{"pres":648.4,"psal":34.257,"temp":3.394},{"pres":653.3,"psal":34.26,"temp":3.385},{"pres":658.3,"psal":34.263,"temp":3.374},{"pres":663.2,"psal":34.265,"temp":3.367},{"pres":668.2,"psal":34.268,"temp":3.357},{"pres":673.1,"psal":34.269,"temp":3.348},{"pres":678.1,"psal":34.271,"temp":3.341},{"pres":683.1,"psal":34.274,"temp":3.332},{"pres":688.1,"psal":34.276,"temp":3.326},{"pres":693.1,"psal":34.278,"temp":3.314},{"pres":698.2,"psal":34.282,"temp":3.297},{"pres":703.3,"psal":34.285,"temp":3.286},{"pres":707.9,"psal":34.287,"temp":3.278},{"pres":712.6,"psal":34.288,"temp":3.273},{"pres":717.9,"psal":34.29,"temp":3.256},{"pres":722.8,"psal":34.293,"temp":3.238},{"pres":728,"psal":34.296,"temp":3.224},{"pres":733.2,"psal":34.299,"temp":3.211},{"pres":738.2,"psal":34.302,"temp":3.202},{"pres":743.1,"psal":34.304,"temp":3.197},{"pres":748.2,"psal":34.306,"temp":3.189},{"pres":753.2,"psal":34.308,"temp":3.184},{"pres":758.3,"psal":34.309,"temp":3.177},{"pres":762.9,"psal":34.311,"temp":3.167},{"pres":767.7,"psal":34.313,"temp":3.155},{"pres":772.8,"psal":34.316,"temp":3.144},{"pres":778.1,"psal":34.319,"temp":3.133},{"pres":783,"psal":34.32,"temp":3.128},{"pres":787.9,"psal":34.323,"temp":3.119},{"pres":792.9,"psal":34.325,"temp":3.107},{"pres":798,"psal":34.328,"temp":3.096},{"pres":803.3,"psal":34.331,"temp":3.081},{"pres":808.2,"psal":34.333,"temp":3.069},{"pres":813.1,"psal":34.334,"temp":3.063},{"pres":818.1,"psal":34.337,"temp":3.054},{"pres":823.1,"psal":34.338,"temp":3.049},{"pres":828.1,"psal":34.339,"temp":3.044},{"pres":833.2,"psal":34.342,"temp":3.033},{"pres":838.3,"psal":34.344,"temp":3.024},{"pres":842.9,"psal":34.345,"temp":3.019},{"pres":847.5,"psal":34.347,"temp":3.009},{"pres":852.6,"psal":34.349,"temp":3.002},{"pres":857.8,"psal":34.35,"temp":2.995},{"pres":863,"psal":34.352,"temp":2.989},{"pres":868.2,"psal":34.352,"temp":2.986},{"pres":872.8,"psal":34.353,"temp":2.984},{"pres":877.5,"psal":34.354,"temp":2.98},{"pres":883.1,"psal":34.356,"temp":2.969},{"pres":888.2,"psal":34.358,"temp":2.96},{"pres":893,"psal":34.36,"temp":2.95},{"pres":897.8,"psal":34.362,"temp":2.943},{"pres":902.8,"psal":34.363,"temp":2.936},{"pres":907.8,"psal":34.365,"temp":2.928},{"pres":912.8,"psal":34.366,"temp":2.924},{"pres":917.9,"psal":34.367,"temp":2.917},{"pres":923,"psal":34.37,"temp":2.905},{"pres":928.2,"psal":34.371,"temp":2.896},{"pres":933,"psal":34.372,"temp":2.891},{"pres":937.9,"psal":34.374,"temp":2.882},{"pres":943.2,"psal":34.376,"temp":2.878},{"pres":948,"psal":34.376,"temp":2.876},{"pres":952.8,"psal":34.376,"temp":2.874},{"pres":958,"psal":34.379,"temp":2.864},{"pres":962.8,"psal":34.381,"temp":2.856},{"pres":967.7,"psal":34.381,"temp":2.852},{"pres":973,"psal":34.383,"temp":2.843},{"pres":977.8,"psal":34.385,"temp":2.833},{"pres":982.7,"psal":34.387,"temp":2.825},{"pres":988.1,"psal":34.39,"temp":2.813},{"pres":992.9,"psal":34.391,"temp":2.808},{"pres":997.8,"psal":34.392,"temp":2.802},{"pres":1005.4,"psal":34.394,"temp":2.79},{"pres":1015.3,"psal":34.397,"temp":2.774},{"pres":1025.6,"psal":34.402,"temp":2.755},{"pres":1035.6,"psal":34.404,"temp":2.744},{"pres":1045.3,"psal":34.407,"temp":2.727},{"pres":1055.2,"psal":34.41,"temp":2.715},{"pres":1065.3,"psal":34.412,"temp":2.703},{"pres":1075.6,"psal":34.414,"temp":2.694},{"pres":1085.6,"psal":34.417,"temp":2.68},{"pres":1095.2,"psal":34.42,"temp":2.663},{"pres":1105.4,"psal":34.424,"temp":2.645},{"pres":1115.8,"psal":34.428,"temp":2.623},{"pres":1125.2,"psal":34.431,"temp":2.611},{"pres":1135.1,"psal":34.432,"temp":2.601},{"pres":1145.4,"psal":34.436,"temp":2.585},{"pres":1155.4,"psal":34.44,"temp":2.569},{"pres":1165.1,"psal":34.443,"temp":2.55},{"pres":1175.5,"psal":34.447,"temp":2.532},{"pres":1185.7,"psal":34.451,"temp":2.511},{"pres":1195.5,"psal":34.453,"temp":2.501},{"pres":1205.4,"psal":34.455,"temp":2.49},{"pres":1215.3,"psal":34.457,"temp":2.477},{"pres":1225.3,"psal":34.461,"temp":2.461},{"pres":1235.4,"psal":34.463,"temp":2.45},{"pres":1245.4,"psal":34.466,"temp":2.439},{"pres":1255.4,"psal":34.468,"temp":2.427},{"pres":1265.5,"psal":34.471,"temp":2.41},{"pres":1275.6,"psal":34.474,"temp":2.396},{"pres":1285.6,"psal":34.476,"temp":2.385},{"pres":1295.6,"psal":34.479,"temp":2.373},{"pres":1305.4,"psal":34.482,"temp":2.361},{"pres":1315.2,"psal":34.484,"temp":2.348},{"pres":1325.2,"psal":34.488,"temp":2.33},{"pres":1335.5,"psal":34.49,"temp":2.323},{"pres":1345.5,"psal":34.492,"temp":2.314},{"pres":1355.4,"psal":34.493,"temp":2.309},{"pres":1365.3,"psal":34.495,"temp":2.301},{"pres":1375.6,"psal":34.498,"temp":2.286},{"pres":1385.2,"psal":34.5,"temp":2.274},{"pres":1395.3,"psal":34.504,"temp":2.259},{"pres":1405.4,"psal":34.505,"temp":2.25},{"pres":1415.4,"psal":34.508,"temp":2.238},{"pres":1425.3,"psal":34.511,"temp":2.223},{"pres":1435.2,"psal":34.513,"temp":2.214},{"pres":1445.3,"psal":34.516,"temp":2.2},{"pres":1455.3,"psal":34.517,"temp":2.195},{"pres":1465.5,"psal":34.519,"temp":2.189},{"pres":1475.3,"psal":34.52,"temp":2.184},{"pres":1485.6,"psal":34.522,"temp":2.173},{"pres":1495.8,"psal":34.524,"temp":2.164},{"pres":1505.4,"psal":34.525,"temp":2.158},{"pres":1515.4,"psal":34.526,"temp":2.153},{"pres":1525.4,"psal":34.528,"temp":2.147},{"pres":1535.5,"psal":34.529,"temp":2.141},{"pres":1545.5,"psal":34.531,"temp":2.134},{"pres":1555.6,"psal":34.532,"temp":2.128},{"pres":1565.8,"psal":34.533,"temp":2.123},{"pres":1575.7,"psal":34.534,"temp":2.119},{"pres":1585.7,"psal":34.535,"temp":2.116},{"pres":1595.7,"psal":34.536,"temp":2.108},{"pres":1605.7,"psal":34.538,"temp":2.099},{"pres":1615.7,"psal":34.54,"temp":2.094},{"pres":1625.6,"psal":34.54,"temp":2.09},{"pres":1635.5,"psal":34.542,"temp":2.083},{"pres":1645.2,"psal":34.543,"temp":2.077},{"pres":1655.3,"psal":34.544,"temp":2.07},{"pres":1665.5,"psal":34.546,"temp":2.063},{"pres":1675.2,"psal":34.548,"temp":2.056},{"pres":1685.1,"psal":34.549,"temp":2.048},{"pres":1695.1,"psal":34.551,"temp":2.041},{"pres":1705,"psal":34.552,"temp":2.035},{"pres":1714.9,"psal":34.554,"temp":2.026},{"pres":1725,"psal":34.555,"temp":2.02},{"pres":1735.5,"psal":34.556,"temp":2.014},{"pres":1745.7,"psal":34.558,"temp":2.007},{"pres":1755.6,"psal":34.559,"temp":2.001},{"pres":1765.3,"psal":34.561,"temp":1.994},{"pres":1775.2,"psal":34.561,"temp":1.989},{"pres":1785.5,"psal":34.562,"temp":1.986},{"pres":1795.5,"psal":34.563,"temp":1.982},{"pres":1805.3,"psal":34.564,"temp":1.979},{"pres":1815.2,"psal":34.565,"temp":1.973},{"pres":1825.2,"psal":34.566,"temp":1.968},{"pres":1835.3,"psal":34.568,"temp":1.961},{"pres":1845.6,"psal":34.569,"temp":1.956},{"pres":1855.6,"psal":34.57,"temp":1.951},{"pres":1865.2,"psal":34.571,"temp":1.946},{"pres":1875.4,"psal":34.572,"temp":1.94},{"pres":1885.7,"psal":34.574,"temp":1.929},{"pres":1895.7,"psal":34.576,"temp":1.922},{"pres":1905.3,"psal":34.577,"temp":1.917},{"pres":1915.6,"psal":34.579,"temp":1.91},{"pres":1925.7,"psal":34.579,"temp":1.906},{"pres":1935.7,"psal":34.581,"temp":1.9},{"pres":1945.5,"psal":34.582,"temp":1.895},{"pres":1955.4,"psal":34.582,"temp":1.891},{"pres":1965.6,"psal":34.584,"temp":1.886},{"pres":1975.4,"psal":34.585,"temp":1.88},{"pres":1985.3,"psal":34.586,"temp":1.875},{"pres":1995.3,"psal":34.587,"temp":1.871},{"pres":2005.6,"psal":34.588,"temp":1.863},{"pres":2013.3,"psal":34.589,"temp":1.858}],"station_parameters":["pres","psal","temp"],"pres_max_for_TEMP":2013.3,"pres_min_for_TEMP":3.4,"pres_max_for_PSAL":2013.3,"pres_min_for_PSAL":3.4,"max_pres":2013.3,"date":"2023-01-26T05:19:00.000Z","date_added":"2023-01-27T08:08:38.602Z","date_qc":1,"lat":54.25397491455078,"lon":-151.1851043701172,"geoLocation":{"type":"Point","coordinates":[-151.1851043701172,54.25397491455078]},"position_qc":1,"cycle_number":56,"dac":"meds","platform_number":4902521,"station_parameters_in_nc":["MTIME","PRES","PSAL","TEMP"],"nc_url":"ftp://ftp.ifremer.fr/ifremer/argo/dac/meds/4902521/profiles/R4902521_056.nc","DIRECTION":"A","BASIN":2,"core_data_mode":"R","roundLat":"54.254","roundLon":"-151.185","strLat":"54.254 N","strLon":"151.185 W","formatted_station_parameters":[" pres"," psal"," temp"]}] \ No newline at end of file From 98579787f1610c1c960a7d5b55abac8464119917 Mon Sep 17 00:00:00 2001 From: Zach Fair <48361714+zachghiaccio@users.noreply.github.com> Date: Fri, 3 Nov 2023 16:04:00 -0400 Subject: [PATCH 096/124] Update doc/source/contributing/quest-available-datasets.rst Co-authored-by: Jessica Scheick --- doc/source/contributing/quest-available-datasets.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/source/contributing/quest-available-datasets.rst b/doc/source/contributing/quest-available-datasets.rst index 6c8d86f15..760e399b9 100644 --- a/doc/source/contributing/quest-available-datasets.rst +++ b/doc/source/contributing/quest-available-datasets.rst @@ -16,7 +16,6 @@ The Argo mission involves a series of floats that are designed to capture vertic A paper outlining the Argo extension to QUEST is currently in preparation, with a citable preprint available in the near future. :ref:`Argo Workflow Example` -(Link to example workbook here) Adding a Dataset to QUEST From 8ed4c7be0bd87bb58511cb9366e228018deefdaa Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Fri, 3 Nov 2023 15:54:05 -0400 Subject: [PATCH 097/124] fix formatting via linter --- icepyx/quest/quest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index 606b4e8c6..b3c4c5e83 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -179,7 +179,6 @@ def search_all(self, **kwargs): except KeyError: msg = v.avail_granules() print(msg) - else: print(k) try: @@ -191,7 +190,6 @@ def search_all(self, **kwargs): dataset_name = type(v).__name__ print("Error querying data from {0}".format(dataset_name)) - # error handling? what happens if the user tries to re-download? def download_all(self, path="", **kwargs): """ From b99855e83be01da87bd11596ff203f05ecd9ddfd Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Fri, 3 Nov 2023 16:25:30 -0400 Subject: [PATCH 098/124] remove examples suggesting an Argo object can be created directly --- icepyx/quest/dataset_scripts/argo.py | 24 ++---------------------- icepyx/quest/quest.py | 7 +++++++ icepyx/tests/test_quest_argo.py | 2 +- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 0319761cb..5b53ca1e8 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -8,32 +8,12 @@ class Argo(DataSet): """ - Initialises an Argo Dataset object - Used to query physical and BGC Argo profiles - - Examples - -------- - # example with profiles available - >>> reg_a = Argo([-154, 30,-143, 37], ['2022-04-12', '2022-04-26']) - >>> reg_a.search_data() - >>> print(reg_a.profiles[['pres', 'temp', 'lat', 'lon']].head()) - pres temp lat lon - 0 3.9 18.608 33.401 -153.913 - 1 5.7 18.598 33.401 -153.913 - 2 7.7 18.588 33.401 -153.913 - 3 9.7 18.462 33.401 -153.913 - 4 11.7 18.378 33.401 -153.913 - - # example with no profiles - >>> reg_a = Argo([-55, 68, -48, 71], ['2019-02-20', '2019-02-28']) - >>> reg_a.search_data() - Warning: Query returned no profiles - Please try different search parameters + Initialises an Argo Dataset object via a Quest object. + Used to query physical and BGC Argo profiles. See Also -------- DataSet - GenQuery """ # Note: it looks like ArgoVis now accepts polygons, not just bounding boxes diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index b3c4c5e83..aafb47bed 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -145,6 +145,13 @@ def add_argo(self, params=["temperature"], presRange=None) -> None: See Also -------- quest.dataset_scripts.argo + icepyx.query.GenQuery + + Examples + -------- + # example with profiles available + >>> reg_a = Quest([-154, 30,-143, 37], ['2022-04-12', '2022-04-26']) + >>> reg_a.add_argo(params=["temperature", "salinity"]) """ argo = Argo(self._spatial, self._temporal, params, presRange) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index 25871805f..cf14f25e3 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -6,7 +6,7 @@ # create an Argo instance via quest (Argo is a submodule) def argo_quest_instance(bounding_box, date_range, params=None): my_quest = Quest(spatial_extent=bounding_box, date_range=date_range) - my_quest.add_argo() + my_quest.add_argo(params) my_argo = my_quest.datasets["argo"] return my_argo From 9aaca81c56b3672b774834ca338038aaae6020dc Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Sat, 4 Nov 2023 15:50:26 +0000 Subject: [PATCH 099/124] Clarified that the new dataset guidelines are a work in progress. --- doc/source/contributing/quest-available-datasets.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/contributing/quest-available-datasets.rst b/doc/source/contributing/quest-available-datasets.rst index 760e399b9..0fd268571 100644 --- a/doc/source/contributing/quest-available-datasets.rst +++ b/doc/source/contributing/quest-available-datasets.rst @@ -23,6 +23,7 @@ Adding a Dataset to QUEST Want to add a new dataset to QUEST? No problem! QUEST includes a template script (``dataset.py``) that may be used to create your own querying module for a dataset of interest. -Guidelines on how to construct your dataset module may be found here: (link to be added) - Once you have developed a script with the template, you may request for the module to be added to QUEST via Github. Please see the How to Contribute page :ref:`dev_guide_label` for instructions on how to contribute to icepyx. + +Detailed guidelines on how to construct your dataset module are currently a work in progress. + From f6eb1b2bab394ac3ddfd7e8d7bae0e2778f8af31 Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Sat, 4 Nov 2023 16:04:15 +0000 Subject: [PATCH 100/124] Cleanup of text in QUEST notebook. --- .../example_notebooks/QUEST_argo_data_access.ipynb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb index 60c2e6762..187529675 100644 --- a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb +++ b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb @@ -45,7 +45,7 @@ "source": [ "## Define the Quest Object\n", "\n", - "The key to using icepyx for multiple datasets is to use the QUEST module. QUEST builds off of the general querying process originally designed for ICESat-2, but makes it applicable to other datasets.\n", + "QUEST builds off of the general querying process originally designed for ICESat-2, but makes it applicable to other datasets.\n", "\n", "Just like the ICESat-2 Query object, we begin by defining our Quest object. We provide the following bounding parameters:\n", "* `spatial_extent`: Data is constrained to the given box over the Pacific Ocean.\n", @@ -142,9 +142,9 @@ "user_expressions": [] }, "source": [ - "Note that many of the ICESat-2 functions shown here are the same as those used for normal icepyx queries. The user is referred to other examples for detailed explanations about other icepyx features.\n", + "Note that many of the ICESat-2 functions shown here are the same as those used for normal icepyx queries. The user is referred to other example workbooks for detailed explanations about additional icepyx features.\n", "\n", - "Downloading ICESat-2 data requires Earthdata login credentials, and the `download_all()` function below gives an authentication check when attempting to access the ICESat-2 files." + "Downloading ICESat-2 data requires Earthdata login credentials. When running the `download_all()` function below, an authentication check will be passed when attempting to download the ICESat-2 files." ] }, { @@ -191,8 +191,10 @@ }, "outputs": [], "source": [ + "path = '/icepyx/quest/downloaded-data/'\n", + "\n", "# Access Argo and ICESat-2 data simultaneously\n", - "reg_a.download_all('/home/jovyan/icesat2-snowex/icepyx/quest-test-data/')" + "reg_a.download_all(path)" ] }, { @@ -219,8 +221,6 @@ "outputs": [], "source": [ "# Load ICESat-2 latitudes, longitudes, heights, and photon confidence (optional)\n", - "path_root = '/home/jovyan/icesat2-snowex/icepyx/quest-test-data/'\n", - "\n", "is2_pd = pd.DataFrame()\n", "with h5py.File(f'{path_root}processed_ATL03_20220419002753_04111506_006_02.h5', 'r') as f:\n", " is2_pd['lat'] = f['gt2l/heights/lat_ph'][:]\n", @@ -386,7 +386,7 @@ "plt.tight_layout()\n", "\n", "# Save figure\n", - "#plt.savefig('/home/jovyan/icepyx/is2_argo_figure.png', dpi=500)" + "#plt.savefig('/icepyx/quest/figures/is2_argo_figure.png', dpi=500)" ] } ], From 7e004f058818581ffa62a4c8f703e4eff6d201b1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 6 Nov 2023 16:29:20 +0000 Subject: [PATCH 101/124] GitHub action UML generation auto-update --- .../user_guide/documentation/classes_dev_uml.svg | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/source/user_guide/documentation/classes_dev_uml.svg b/doc/source/user_guide/documentation/classes_dev_uml.svg index 0cd08c9e9..77c2cb4b8 100644 --- a/doc/source/user_guide/documentation/classes_dev_uml.svg +++ b/doc/source/user_guide/documentation/classes_dev_uml.svg @@ -33,13 +33,13 @@ EarthdataAuthMixin -_auth : Auth, NoneType -_s3_initial_ts : NoneType, datetime -_s3login_credentials : NoneType, dict -_session : NoneType -auth -s3login_credentials -session +_auth : NoneType +_s3_initial_ts : NoneType, datetime +_s3login_credentials : NoneType +_session : NoneType +auth +s3login_credentials +session __init__(auth) __str__() From e8f8c387735d4d2f2a3146a57d51859cef6bfa2d Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 6 Nov 2023 13:01:26 -0500 Subject: [PATCH 102/124] fix failing argo tests --- icepyx/tests/test_quest_argo.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index cf14f25e3..f77f0b70f 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -4,9 +4,9 @@ from icepyx.quest.quest import Quest # create an Argo instance via quest (Argo is a submodule) -def argo_quest_instance(bounding_box, date_range, params=None): +def argo_quest_instance(bounding_box, date_range): my_quest = Quest(spatial_extent=bounding_box, date_range=date_range) - my_quest.add_argo(params) + my_quest.add_argo() my_argo = my_quest.datasets["argo"] return my_argo @@ -99,7 +99,8 @@ def test_merge_df(): def test_presRange_input_param(): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) - df = reg_a.download(params=["salinity"], presRange="0.2,100") + df = reg_a.download(params=["salinity"], presRange="0.2,180") assert df["pressure"].min() >= 0.2 - assert df["pressure"].max() <= 100 + assert df["pressure"].max() <= 180 + assert "salinity" in df.columns From d5747fae827ae734e9fa329371c2bde3fa3995bd Mon Sep 17 00:00:00 2001 From: Rachel Wegener <35503632+rwegener2@users.noreply.github.com> Date: Tue, 7 Nov 2023 07:17:42 -0500 Subject: [PATCH 103/124] Variables as an independent class (#451) Refactor Variables class to be user facing functionality --- .../IS2_data_access2-subsetting.ipynb | 42 +- .../IS2_data_variables.ipynb | 351 ++++++++++++- .../documentation/classes_dev_uml.svg | 497 +++++++++--------- .../documentation/classes_user_uml.svg | 33 +- .../user_guide/documentation/components.rst | 8 - .../user_guide/documentation/icepyx.rst | 1 + .../documentation/packages_user_uml.svg | 60 ++- .../user_guide/documentation/variables.rst | 25 + icepyx/__init__.py | 1 + icepyx/core/is2ref.py | 53 +- icepyx/core/query.py | 51 +- icepyx/core/read.py | 59 ++- icepyx/core/variables.py | 160 +++--- 13 files changed, 880 insertions(+), 461 deletions(-) create mode 100644 doc/source/user_guide/documentation/variables.rst diff --git a/doc/source/example_notebooks/IS2_data_access2-subsetting.ipynb b/doc/source/example_notebooks/IS2_data_access2-subsetting.ipynb index 89247de5f..3803b9fd6 100644 --- a/doc/source/example_notebooks/IS2_data_access2-subsetting.ipynb +++ b/doc/source/example_notebooks/IS2_data_access2-subsetting.ipynb @@ -51,7 +51,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "Create a query object and log in to Earthdata\n", "\n", @@ -83,7 +85,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "## Discover Subsetting Options\n", "\n", @@ -108,7 +112,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "By default, spatial and temporal subsetting based on your initial inputs is applied to your order unless you specify `subset=False` to `order_granules()` or `download_granules()` (which calls `order_granules` under the hood if you have not already placed your order) functions.\n", "Additional subsetting options must be specified as keyword arguments to the order/download functions.\n", @@ -118,7 +124,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "### _Why do I have to provide spatial bounds to icepyx even if I don't use them to subset my data order?_\n", "\n", @@ -132,7 +140,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "## About Data Variables in a query object\n", "\n", @@ -145,7 +155,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "### Determine what variables are available for your data product\n", "There are multiple ways to get a complete list of available variables.\n", @@ -159,7 +171,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "region_a.order_vars.avail()" @@ -167,7 +181,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "By passing the boolean `options=True` to the `avail` method, you can obtain lists of unique possible variable inputs (var_list inputs) and path subdirectory inputs (keyword_list and beam_list inputs) for your data product. These can be helpful for building your wanted variable list." ] @@ -175,7 +191,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "region_a.order_vars.avail(options=True)" @@ -353,9 +371,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "icepyx-dev", "language": "python", - "name": "python3" + "name": "icepyx-dev" }, "language_info": { "codemirror_mode": { @@ -367,7 +385,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/doc/source/example_notebooks/IS2_data_variables.ipynb b/doc/source/example_notebooks/IS2_data_variables.ipynb index 3ac1f99fe..78a250789 100644 --- a/doc/source/example_notebooks/IS2_data_variables.ipynb +++ b/doc/source/example_notebooks/IS2_data_variables.ipynb @@ -2,7 +2,9 @@ "cells": [ { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "# ICESat-2's Nested Variables\n", "\n", @@ -13,10 +15,10 @@ "\n", "A given ICESat-2 product may have over 200 variable + path combinations.\n", "icepyx includes a custom `Variables` module that is \"aware\" of the ATLAS sensor and how the ICESat-2 data products are stored.\n", - "The module can be accessed independently, but is optimally used as a component of a `Query` object (Case 1) or `Read` object (Case 2).\n", + "The module can be accessed independently, and can also be accessed as a component of a `Query` object or `Read` object.\n", "\n", - "This notebook illustrates in detail how the `Variables` module behaves using a `Query` data access example.\n", - "However, module usage is analogous through an icepyx ICESat-2 `Read` object.\n", + "This notebook illustrates in detail how the `Variables` module behaves. We use the module independently and also show how powerful it is directly in the icepyx workflow using a `Query` data access example.\n", + "Module usage using `Query` is analogous through an icepyx ICESat-2 `Read` object.\n", "More detailed example workflows specifically for the [query](https://icepyx.readthedocs.io/en/latest/example_notebooks/IS2_data_access.html) and [read](https://icepyx.readthedocs.io/en/latest/example_notebooks/IS2_data_read-in.html) tools within icepyx are available as separate Jupyter Notebooks.\n", "\n", "Questions? Be sure to check out the FAQs throughout this notebook, indicated as italic headings." @@ -24,11 +26,15 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "### _Why do ICESat-2 products need a custom variable manager?_\n", "\n", "_It can be confusing and cumbersome to comb through the 200+ variable and path combinations contained in ICESat-2 data products._\n", + "_An hdf5 file is built like a folder with files in it. Opening an ICESat-2 file can be like opening a new folder with over 200 files in it and manually searching for only ones you want!_\n", + "\n", "_The icepyx `Variables` module makes it easier for users to quickly find and extract the specific variables they would like to work with across multiple beams, keywords, and variables and provides reader-friendly formatting to browse variables._\n", "_A future development goal for `icepyx` includes developing an interactive widget to further improve the user experience._\n", "_For data read-in, additional tools are available to target specific beam characteristics (e.g. strong versus weak beams)._" @@ -38,35 +44,245 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Some technical details about the Variables module\n", - "For those eager to push the limits or who want to know more implementation details...\n", + "Import packages, including icepyx" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import icepyx as ipx\n", + "from pprint import pprint" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "user_expressions": [] + }, + "source": [ + "## Creating or Accessing ICESat-2 Variables" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "user_expressions": [] + }, + "source": [ + "There are three ways to create or access an ICESat-2 Variables object in icepyx:\n", + "1. Access via the `.order_vars` property of a Query object\n", + "2. Access via the `.vars` property of a Read object\n", + "3. Create a stand-alone ICESat-2 Variables object using a local file or a product name\n", "\n", - "The only required input to the `Variables` module is `vartype`.\n", - "`vartype` has two acceptible string values, 'order' and 'file'.\n", - "If you use the module as shown in icepyx examples (namely through a `Read` or `Query` object), then this flag will be passed automatically.\n", - "It simply tells the software how to generate the list of possible variable values - either by pinging NSIDC for a list of available variables (`query`) or from the user-supplied file (`read`)." + "An example of each of these is shown below." ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ - "Import packages, including icepyx" + "### 1. Access `Variables` via the `.order_vars` property of a Query object" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "import icepyx as ipx\n", - "from pprint import pprint" + "region_a = ipx.Query('ATL06',[-55, 68, -48, 71],['2019-02-22','2019-02-28'], \\\n", + " start_time='00:00:00', end_time='23:59:59')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Accessing Variables\n", + "region_a.order_vars" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Showing the variable paths\n", + "region_a.order_vars.avail()" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "tags": [], + "user_expressions": [] + }, + "source": [ + "### 2. Access via the `.vars` property of a Read object" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "path_root = '/full/path/to/your/data/'\n", + "reader = ipx.Read(path_root)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Accessing Variables\n", + "reader.vars" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Showing the variable paths\n", + "# reader.vars.avail()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "user_expressions": [] + }, + "source": [ + "### 3. Create a stand-alone Variables object\n", + "\n", + "You can also generate an independent Variables object. This can be done using either:\n", + "1. The filepath to a file you'd like a variables list for\n", + "2. The product name (and optionally version) of a an ICESat-2 product" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "user_expressions": [] + }, + "source": [ + "Create a variables object from a filepath:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "filepath = '/full/path/to/your/data.h5'\n", + "v = ipx.Variables(path=filepath)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# v.avail()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "user_expressions": [] + }, + "source": [ + "Create a variables object from a product. The version argument is optional." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "v = ipx.Variables(product='ATL03')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# v.avail()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "v = ipx.Variables(product='ATL03', version='004')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# v.avail()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "user_expressions": [] + }, + "source": [ + "Now that you know how to create or access Variables the remainder of this notebook showcases the functions availble for building and modifying variables lists. Remember, the example shown below uses a Query object, but the same methods are available if you are using a Read object or a Variables object." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "user_expressions": [] + }, "source": [ "## Interacting with ICESat-2 Data Variables\n", "\n", @@ -88,7 +304,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "Create a query object and log in to Earthdata\n", "\n", @@ -134,7 +352,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "### ICESat-2 data variables\n", "\n", @@ -157,7 +377,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "To increase readability, you can use built in functions to show the 200+ variable + path combinations as a dictionary where the keys are variable names and the values are the paths to that variable.\n", "`region_a.order_vars.parse_var_list(region_a.order_vars.avail())` will return a dictionary of variable:paths key:value pairs." @@ -174,7 +396,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "By passing the boolean `options=True` to the `avail` method, you can obtain lists of unique possible variable inputs (var_list inputs) and path subdirectory inputs (keyword_list and beam_list inputs) for your data product. These can be helpful for building your wanted variable list." ] @@ -188,6 +412,30 @@ "region_a.order_vars.avail(options=True)" ] }, + { + "cell_type": "markdown", + "metadata": { + "user_expressions": [] + }, + "source": [ + "```{admonition} Remember\n", + "You can run these same methods no matter how you created or accessed your ICESat-2 Variables. So the methods in this section could be equivalently be accessed using a Read object, or by directly accessing a file on your computer:\n", + "\n", + "```\n", + "```python\n", + "# Using a Read object\n", + "reader.vars.avail()\n", + "reader.vars.parse_var_list(reader.vars.avail())\n", + "reader.vars.avail(options=True)\n", + "\n", + "# Using a file on your computer\n", + "v = Variables(path='/my/file.h5')\n", + "v.avail()\n", + "v.parse_var_list(v.avail())\n", + "v.avail(options=True)\n", + "```\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -228,7 +476,9 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "The keywords available for this product are shown in the error message upon entering a blank keyword_list, as seen in the next cell." ] @@ -745,13 +995,62 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "user_expressions": [] + }, "source": [ "#### With a `Read` object\n", "Calling the `load()` method on your `Read` object will automatically look for your wanted variable list and use it.\n", "Please see the [read-in example Jupyter Notebook](https://icepyx.readthedocs.io/en/latest/example_notebooks/IS2_data_read-in.html) for a complete example of this usage.\n" ] }, + { + "cell_type": "markdown", + "metadata": { + "user_expressions": [] + }, + "source": [ + "#### With a local filepath\n", + "\n", + "One of the benefits of using a local filepath in variables is that it allows you to easily inspect the variables that are available in your file. Once you have a variable of interest from the `avail` list, you could read that variable in with another library, such as xarray. The example below demonstrates this assuming an ATL06 ICESat-2 file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "filepath = '/full/path/to/my/ATL06_file.h5'\n", + "v = ipx.Variables(path=filepath)\n", + "v.avail()\n", + "# Browse paths and decide you need `gt1l/land_ice_segments/`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import xarray as xr\n", + "\n", + "xr.open_dataset(filepath, group='gt1l/land_ice_segments/', engine='h5netcdf')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "user_expressions": [] + }, + "source": [ + "You'll notice in this workflow you are limited to viewing data only within a particular group. Icepyx also provides functionality for merging variables within or even across files. See the [read-in example Jupyter Notebook](https://icepyx.readthedocs.io/en/latest/example_notebooks/IS2_data_read-in.html) for more details about these features of icepyx." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -763,9 +1062,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "icepyx-dev", "language": "python", - "name": "python3" + "name": "icepyx-dev" }, "language_info": { "codemirror_mode": { @@ -777,7 +1076,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/doc/source/user_guide/documentation/classes_dev_uml.svg b/doc/source/user_guide/documentation/classes_dev_uml.svg index 0cd08c9e9..765e0d531 100644 --- a/doc/source/user_guide/documentation/classes_dev_uml.svg +++ b/doc/source/user_guide/documentation/classes_dev_uml.svg @@ -4,328 +4,329 @@ - - + + classes_dev_uml - + icepyx.core.auth.AuthenticationError - -AuthenticationError - - - + +AuthenticationError + + + icepyx.core.exceptions.DeprecationError - -DeprecationError - - - + +DeprecationError + + + icepyx.core.auth.EarthdataAuthMixin - -EarthdataAuthMixin - -_auth : Auth, NoneType -_s3_initial_ts : NoneType, datetime -_s3login_credentials : NoneType, dict -_session : NoneType -auth -s3login_credentials -session - -__init__(auth) -__str__() -earthdata_login(uid, email, s3token): None + +EarthdataAuthMixin + +_auth : NoneType +_s3_initial_ts : NoneType, datetime +_s3login_credentials : NoneType +_session : NoneType +auth +s3login_credentials +session + +__init__(auth) +__str__() +earthdata_login(uid, email, s3token): None icepyx.core.query.GenQuery - -GenQuery - -_spatial -_temporal -dates -end_time -spatial -spatial_extent -start_time -temporal - -__init__(spatial_extent, date_range, start_time, end_time) -__str__() + +GenQuery + +_spatial +_temporal +dates +end_time +spatial +spatial_extent +start_time +temporal + +__init__(spatial_extent, date_range, start_time, end_time) +__str__() icepyx.core.granules.Granules - -Granules - -avail : list -orderIDs : list - -__init__ -() -download(verbose, path, session, restart) -get_avail(CMRparams, reqparams, cloud) -place_order(CMRparams, reqparams, subsetparams, verbose, subset, session, geom_filepath) + +Granules + +avail : list +orderIDs : list + +__init__ +() +download(verbose, path, session, restart) +get_avail(CMRparams, reqparams, cloud) +place_order(CMRparams, reqparams, subsetparams, verbose, subset, session, geom_filepath) icepyx.core.query.Query - -Query - -CMRparams -_CMRparams -_about_product -_cust_options : dict -_cycles : list -_file_vars -_granules -_order_vars -_prod : NoneType, str -_readable_granule_name : list -_reqparams -_source : str -_subsetparams : NoneType -_tracks : list -_version -cycles -dataset -file_vars -granules -order_vars -product -product_version -reqparams -tracks - -__init__(product, spatial_extent, date_range, start_time, end_time, version, cycles, tracks, files, auth) -__str__() -avail_granules(ids, cycles, tracks, cloud) -download_granules(path, verbose, subset, restart) -latest_version() -order_granules(verbose, subset, email) -product_all_info() -product_summary_info() -show_custom_options(dictview) -subsetparams() -visualize_elevation() -visualize_spatial_extent() + +Query + +CMRparams +_CMRparams +_about_product +_cust_options : dict +_cycles : list +_file_vars +_granules +_order_vars +_prod : NoneType, str +_readable_granule_name : list +_reqparams +_source : str +_subsetparams : NoneType +_tracks : list +_version +cycles +dataset +file_vars +granules +order_vars +product +product_version +reqparams +tracks + +__init__(product, spatial_extent, date_range, start_time, end_time, version, cycles, tracks, files, auth) +__str__() +avail_granules(ids, cycles, tracks, cloud) +download_granules(path, verbose, subset, restart) +latest_version() +order_granules(verbose, subset, email) +product_all_info() +product_summary_info() +show_custom_options(dictview) +subsetparams() +visualize_elevation() +visualize_spatial_extent() icepyx.core.granules.Granules->icepyx.core.query.Query - - -_granules + + +_granules icepyx.core.granules.Granules->icepyx.core.query.Query - - -_granules + + +_granules icepyx.core.icesat2data.Icesat2Data - -Icesat2Data - - -__init__() + +Icesat2Data + + +__init__() icepyx.core.exceptions.NsidcQueryError - -NsidcQueryError - -errmsg -msgtxt : str - -__init__(errmsg, msgtxt) -__str__() + +NsidcQueryError + +errmsg +msgtxt : str + +__init__(errmsg, msgtxt) +__str__() icepyx.core.exceptions.QueryError - -QueryError - - - + +QueryError + + + icepyx.core.exceptions.NsidcQueryError->icepyx.core.exceptions.QueryError - - + + icepyx.core.APIformatting.Parameters - -Parameters - -_fmted_keys : NoneType, dict -_poss_keys : dict -_reqtype : NoneType, str -fmted_keys -partype -poss_keys - -__init__(partype, values, reqtype) -_check_valid_keys() -_get_possible_keys() -build_params() -check_req_values() -check_values() + +Parameters + +_fmted_keys : NoneType, dict +_poss_keys : dict +_reqtype : NoneType, str +fmted_keys +partype +poss_keys + +__init__(partype, values, reqtype) +_check_valid_keys() +_get_possible_keys() +build_params() +check_req_values() +check_values() icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - -_CMRparams + + +_CMRparams icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - -_reqparams + + +_reqparams icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - -_subsetparams + + +_subsetparams icepyx.core.APIformatting.Parameters->icepyx.core.query.Query - - -_subsetparams + + +_subsetparams icepyx.core.query.Query->icepyx.core.auth.EarthdataAuthMixin - - + + icepyx.core.query.Query->icepyx.core.query.GenQuery - - + + icepyx.core.read.Read - -Read - -_filelist : NoneType, list -_out_obj : Dataset -_product : NoneType, str -_read_vars -filelist -product -vars - -__init__(data_source, product, filename_pattern, catalog, glob_kwargs, out_obj_type) -_add_vars_to_ds(is2ds, ds, grp_path, wanted_groups_tiered, wanted_dict) -_build_dataset_template(file) -_build_single_file_dataset(file, groups_list) -_check_source_for_pattern(source, filename_pattern) -_combine_nested_vars(is2ds, ds, grp_path, wanted_dict) -_extract_product(filepath) -_read_single_grp(file, grp_path) -load() + +Read + +_filelist : NoneType, list +_out_obj : Dataset +_product : NoneType, str +_read_vars +filelist +product +vars + +__init__(data_source, product, filename_pattern, catalog, glob_kwargs, out_obj_type) +_add_vars_to_ds(is2ds, ds, grp_path, wanted_groups_tiered, wanted_dict) +_build_dataset_template(file) +_build_single_file_dataset(file, groups_list) +_check_source_for_pattern(source, filename_pattern) +_combine_nested_vars(is2ds, ds, grp_path, wanted_dict) +_read_single_grp(file, grp_path) +load() icepyx.core.spatial.Spatial - -Spatial - -_ext_type : str -_gdf_spat : GeoDataFrame -_geom_file : NoneType -_spatial_ext -_xdateln -extent -extent_as_gdf -extent_file -extent_type - -__init__(spatial_extent) -__str__() -fmt_for_CMR() -fmt_for_EGI() + +Spatial + +_ext_type : str +_gdf_spat : GeoDataFrame +_geom_file : NoneType +_spatial_ext +_xdateln +extent +extent_as_gdf +extent_file +extent_type + +__init__(spatial_extent) +__str__() +fmt_for_CMR() +fmt_for_EGI() icepyx.core.spatial.Spatial->icepyx.core.query.GenQuery - - -_spatial + + +_spatial icepyx.core.spatial.Spatial->icepyx.core.query.GenQuery - - -_spatial + + +_spatial icepyx.core.temporal.Temporal - -Temporal - -_end : datetime -_start : datetime -end -start - -__init__(date_range, start_time, end_time) -__str__() + +Temporal + +_end : datetime +_start : datetime +end +start + +__init__(date_range, start_time, end_time) +__str__() icepyx.core.temporal.Temporal->icepyx.core.query.GenQuery - - -_temporal + + +_temporal icepyx.core.variables.Variables - -Variables - -_avail : NoneType, list -_vartype -_version : NoneType -path : NoneType -product : NoneType -wanted : NoneType, dict + +Variables + +_avail : NoneType, list +_path : NoneType +_product : NoneType, str +_version +path +product +version +wanted : NoneType, dict -__init__(vartype, avail, wanted, product, version, path, auth) +__init__(vartype, path, product, version, avail, wanted, auth) _check_valid_lists(vgrp, allpaths, var_list, beam_list, keyword_list) _get_combined_list(beam_list, keyword_list) _get_sum_varlist(var_list, all_vars, defaults) @@ -339,57 +340,57 @@ icepyx.core.variables.Variables->icepyx.core.auth.EarthdataAuthMixin - - + + icepyx.core.variables.Variables->icepyx.core.query.Query - - -_order_vars + + +_order_vars icepyx.core.variables.Variables->icepyx.core.query.Query - - -_order_vars + + +_order_vars icepyx.core.variables.Variables->icepyx.core.query.Query - - -_file_vars + + +_file_vars icepyx.core.variables.Variables->icepyx.core.read.Read - - -_read_vars + + +_read_vars icepyx.core.visualization.Visualize - -Visualize - -bbox : list -cycles : NoneType -date_range : NoneType -product : NoneType, str -tracks : NoneType - -__init__(query_obj, product, spatial_extent, date_range, cycles, tracks) -generate_OA_parameters(): list -grid_bbox(binsize): list -make_request(base_url, payload) -parallel_request_OA(): da.array -query_icesat2_filelist(): tuple -request_OA_data(paras): da.array -viz_elevation(): (hv.DynamicMap, hv.Layout) + +Visualize + +bbox : list +cycles : NoneType +date_range : NoneType +product : NoneType, str +tracks : NoneType + +__init__(query_obj, product, spatial_extent, date_range, cycles, tracks) +generate_OA_parameters(): list +grid_bbox(binsize): list +make_request(base_url, payload) +parallel_request_OA(): da.array +query_icesat2_filelist(): tuple +request_OA_data(paras): da.array +viz_elevation(): (hv.DynamicMap, hv.Layout) diff --git a/doc/source/user_guide/documentation/classes_user_uml.svg b/doc/source/user_guide/documentation/classes_user_uml.svg index a9c116469..59b8e8e6f 100644 --- a/doc/source/user_guide/documentation/classes_user_uml.svg +++ b/doc/source/user_guide/documentation/classes_user_uml.svg @@ -259,49 +259,50 @@ icepyx.core.variables.Variables - -Variables - -path : NoneType -product : NoneType -wanted : NoneType, dict - -append(defaults, var_list, beam_list, keyword_list) -avail(options, internal) -parse_var_list(varlist, tiered, tiered_vars) -remove(all, var_list, beam_list, keyword_list) + +Variables + +path +product +version +wanted : NoneType, dict + +append(defaults, var_list, beam_list, keyword_list) +avail(options, internal) +parse_var_list(varlist, tiered, tiered_vars) +remove(all, var_list, beam_list, keyword_list) icepyx.core.variables.Variables->icepyx.core.auth.EarthdataAuthMixin - + icepyx.core.variables.Variables->icepyx.core.query.Query - + _order_vars icepyx.core.variables.Variables->icepyx.core.query.Query - + _order_vars icepyx.core.variables.Variables->icepyx.core.query.Query - + _file_vars icepyx.core.variables.Variables->icepyx.core.read.Read - + _read_vars diff --git a/doc/source/user_guide/documentation/components.rst b/doc/source/user_guide/documentation/components.rst index b4b658385..dea41a970 100644 --- a/doc/source/user_guide/documentation/components.rst +++ b/doc/source/user_guide/documentation/components.rst @@ -67,14 +67,6 @@ validate\_inputs :undoc-members: :show-inheritance: -variables ---------- - -.. automodule:: icepyx.core.variables - :members: - :undoc-members: - :show-inheritance: - visualize --------- diff --git a/doc/source/user_guide/documentation/icepyx.rst b/doc/source/user_guide/documentation/icepyx.rst index 56ff7f496..a8a9a6f8e 100644 --- a/doc/source/user_guide/documentation/icepyx.rst +++ b/doc/source/user_guide/documentation/icepyx.rst @@ -23,4 +23,5 @@ Diagrams are updated automatically after a pull request (PR) is approved and bef query read quest + variables components diff --git a/doc/source/user_guide/documentation/packages_user_uml.svg b/doc/source/user_guide/documentation/packages_user_uml.svg index 44a041c77..8d8cf0dc9 100644 --- a/doc/source/user_guide/documentation/packages_user_uml.svg +++ b/doc/source/user_guide/documentation/packages_user_uml.svg @@ -4,11 +4,11 @@ - + packages_user_uml - + icepyx.core @@ -24,14 +24,14 @@ icepyx.core.auth - -icepyx.core.auth + +icepyx.core.auth icepyx.core.exceptions - -icepyx.core.exceptions + +icepyx.core.exceptions @@ -42,14 +42,14 @@ icepyx.core.icesat2data - -icepyx.core.icesat2data + +icepyx.core.icesat2data icepyx.core.is2ref - -icepyx.core.is2ref + +icepyx.core.is2ref @@ -60,8 +60,8 @@ icepyx.core.query->icepyx.core.auth - - + + @@ -96,44 +96,50 @@ icepyx.core.read - -icepyx.core.read + +icepyx.core.read icepyx.core.read->icepyx.core.exceptions - - + + icepyx.core.read->icepyx.core.variables - - + + icepyx.core.spatial - -icepyx.core.spatial + +icepyx.core.spatial icepyx.core.temporal - -icepyx.core.temporal + +icepyx.core.temporal icepyx.core.validate_inputs - -icepyx.core.validate_inputs + +icepyx.core.validate_inputs icepyx.core.variables->icepyx.core.auth - - + + + + + +icepyx.core.variables->icepyx.core.exceptions + + diff --git a/doc/source/user_guide/documentation/variables.rst b/doc/source/user_guide/documentation/variables.rst new file mode 100644 index 000000000..e147bfd64 --- /dev/null +++ b/doc/source/user_guide/documentation/variables.rst @@ -0,0 +1,25 @@ +Variables Class +================= + +.. currentmodule:: icepyx + + +Constructor +----------- + +.. autosummary:: + :toctree: ../../_icepyx/ + + Variables + + +Methods +------- + +.. autosummary:: + :toctree: ../../_icepyx/ + + Variables.avail + Variables.parse_var_list + Variables.append + Variables.remove diff --git a/icepyx/__init__.py b/icepyx/__init__.py index 3d92e2e60..40ea9e1ec 100644 --- a/icepyx/__init__.py +++ b/icepyx/__init__.py @@ -1,5 +1,6 @@ from icepyx.core.query import Query, GenQuery from icepyx.core.read import Read from icepyx.quest.quest import Quest +from icepyx.core.variables import Variables from _icepyx_version import version as __version__ diff --git a/icepyx/core/is2ref.py b/icepyx/core/is2ref.py index 5faaef110..a90c8fafa 100644 --- a/icepyx/core/is2ref.py +++ b/icepyx/core/is2ref.py @@ -1,3 +1,4 @@ +import h5py import json import numpy as np import requests @@ -110,7 +111,11 @@ def _get_custom_options(session, product, version): # reformatting formats = [Format.attrib for Format in root.iter("Format")] format_vals = [formats[i]["value"] for i in range(len(formats))] - format_vals.remove("") + try: + format_vals.remove("") + except KeyError: + # ATL23 does not have an empty value + pass cust_options.update({"fileformats": format_vals}) # reprojection only applicable on ICESat-2 L3B products. @@ -324,3 +329,49 @@ def gt2spot(gt, sc_orient): raise ValueError("Could not compute the spot number.") return np.uint8(spot) + +def latest_version(product): + """ + Determine the most recent version available for the given product. + + Examples + -------- + >>> latest_version('ATL03') + '006' + """ + _about_product = about_product(product) + return max( + [entry["version_id"] for entry in _about_product["feed"]["entry"]] + ) + +def extract_product(filepath): + """ + Read the product type from the metadata of the file. Return the product as a string. + """ + with h5py.File(filepath, 'r') as f: + try: + product = f.attrs['short_name'] + if isinstance(product, bytes): + # For most products the short name is stored in a bytes string + product = product.decode() + elif isinstance(product, np.ndarray): + # ATL14 saves the short_name as an array ['ATL14'] + product = product[0] + product = _validate_product(product) + except KeyError: + raise 'Unable to parse the product name from file metadata' + return product + +def extract_version(filepath): + """ + Read the version from the metadata of the file. Return the version as a string. + """ + with h5py.File(filepath, 'r') as f: + try: + version = f['METADATA']['DatasetIdentification'].attrs['VersionID'] + if isinstance(version, np.ndarray): + # ATL14 stores the version as an array ['00x'] + version = version[0] + except KeyError: + raise 'Unable to parse the version from file metadata' + return version diff --git a/icepyx/core/query.py b/icepyx/core/query.py index 3459fd132..8700d5655 100644 --- a/icepyx/core/query.py +++ b/icepyx/core/query.py @@ -12,6 +12,7 @@ import icepyx.core.APIformatting as apifmt from icepyx.core.auth import EarthdataAuthMixin import icepyx.core.granules as granules + # QUESTION: why doesn't from granules import Granules work, since granules=icepyx.core.granules? from icepyx.core.granules import Granules import icepyx.core.is2ref as is2ref @@ -432,7 +433,7 @@ def __init__( super().__init__(spatial_extent, date_range, start_time, end_time, **kwargs) - self._version = val.prod_version(self.latest_version(), version) + self._version = val.prod_version(is2ref.latest_version(self._prod), version) # build list of available CMR parameters if reducing by cycle or RGT # or a list of explicitly named files (full or partial names) @@ -448,6 +449,7 @@ def __init__( # initialize authentication properties EarthdataAuthMixin.__init__(self) + # ---------------------------------------------------------------------- # Properties @@ -646,6 +648,27 @@ def subsetparams(self, **kwargs): if self._subsetparams == None and not kwargs: return {} else: + # If the user has supplied a subset list of variables, append the + # icepyx required variables to the Coverage dict + if "Coverage" in kwargs.keys(): + var_list = [ + "orbit_info/sc_orient", + "orbit_info/sc_orient_time", + "ancillary_data/atlas_sdp_gps_epoch", + "orbit_info/cycle_number", + "orbit_info/rgt", + "ancillary_data/data_start_utc", + "ancillary_data/data_end_utc", + "ancillary_data/granule_start_utc", + "ancillary_data/granule_end_utc", + "ancillary_data/start_delta_time", + "ancillary_data/end_delta_time", + ] + # Add any variables from var_list to Coverage that are not already included + for var in var_list: + if var not in kwargs["Coverage"].keys(): + kwargs["Coverage"][var.split("/")[-1]] = [var] + if self._subsetparams == None: self._subsetparams = apifmt.Parameters("subset") if self._spatial._geom_file is not None: @@ -688,17 +711,16 @@ def order_vars(self): # DevGoal: check for active session here if hasattr(self, "_cust_options"): self._order_vars = Variables( - self._source, - auth = self.auth, product=self.product, + version=self._version, avail=self._cust_options["variables"], + auth=self.auth, ) else: self._order_vars = Variables( - self._source, - auth=self.auth, product=self.product, version=self._version, + auth=self.auth, ) # I think this is where property setters come in, and one should be used here? Right now order_vars.avail is only filled in @@ -722,17 +744,18 @@ def file_vars(self): Examples -------- >>> reg_a = ipx.Query('ATL06',[-55, 68, -48, 71],['2019-02-20','2019-02-28']) # doctest: +SKIP - + >>> reg_a.file_vars # doctest: +SKIP """ if not hasattr(self, "_file_vars"): if self._source == "file": - self._file_vars = Variables(self._source, - auth=self.auth, - product=self.product, - ) + self._file_vars = Variables( + auth=self.auth, + product=self.product, + version=self._version, + ) return self._file_vars @@ -815,6 +838,8 @@ def product_all_info(self): def latest_version(self): """ + A reference function to is2ref.latest_version. + Determine the most recent version available for the given product. Examples @@ -823,11 +848,7 @@ def latest_version(self): >>> reg_a.latest_version() '006' """ - if not hasattr(self, "_about_product"): - self._about_product = is2ref.about_product(self._prod) - return max( - [entry["version_id"] for entry in self._about_product["feed"]["entry"]] - ) + return is2ref.latest_version(self.product) def show_custom_options(self, dictview=False): """ diff --git a/icepyx/core/read.py b/icepyx/core/read.py index a85ee659b..842eab51f 100644 --- a/icepyx/core/read.py +++ b/icepyx/core/read.py @@ -320,10 +320,10 @@ class Read: # ---------------------------------------------------------------------- # Constructors - + def __init__( self, - data_source=None, + data_source=None, # DevNote: Make this a required arg when catalog is removed product=None, filename_pattern=None, catalog=None, @@ -336,10 +336,9 @@ def __init__( "The `catalog` argument has been deprecated and intake is no longer supported. " "Please use the `data_source` argument to specify your dataset instead." ) - + if data_source is None: raise ValueError("data_source is a required arguemnt") - # Raise warnings for deprecated arguments if filename_pattern: warnings.warn( @@ -380,7 +379,7 @@ def __init__( # Create a dictionary of the products as read from the metadata product_dict = {} for file_ in self._filelist: - product_dict[file_] = self._extract_product(file_) + product_dict[file_] = is2ref.extract_product(file_) # Raise warnings or errors for multiple products or products not matching the user-specified product all_products = list(set(product_dict.values())) @@ -456,12 +455,9 @@ def vars(self): """ if not hasattr(self, "_read_vars"): - self._read_vars = Variables( - "file", path=self.filelist[0], product=self.product - ) - + self._read_vars = Variables(path=self.filelist[0]) return self._read_vars - + @property def filelist(self): """ @@ -478,22 +474,6 @@ def product(self): # ---------------------------------------------------------------------- # Methods - - @staticmethod - def _extract_product(filepath): - """ - Read the product type from the metadata of the file. Return the product as a string. - """ - with h5py.File(filepath, "r") as f: - try: - product = f.attrs["short_name"].decode() - product = is2ref._validate_product(product) - except KeyError: - raise AttributeError( - f"Unable to extract the product name from file metadata." - ) - return product - @staticmethod def _check_source_for_pattern(source, filename_pattern): """ @@ -742,8 +722,33 @@ def load(self): # so to get a combined dataset, we need to keep track of spots under the hood, open each group, and then combine them into one xarray where the spots are IDed somehow (or only the strong ones are returned) # this means we need to get/track from each dataset we open some of the metadata, which we include as mandatory variables when constructing the wanted list + if not self.vars.wanted: + raise AttributeError( + 'No variables listed in self.vars.wanted. Please use the Variables class ' + 'via self.vars to search for desired variables to read and self.vars.append(...) ' + 'to add variables to the wanted variables list.' + ) + + # Append the minimum variables needed for icepyx to merge the datasets + # Skip products which do not contain required variables + if self.product not in ['ATL14', 'ATL15', 'ATL23']: + var_list=[ + "sc_orient", + "atlas_sdp_gps_epoch", + "cycle_number", + "rgt", + "data_start_utc", + "data_end_utc", + ] + + # Adjust the nec_varlist for individual products + if self.product == "ATL11": + var_list.remove("sc_orient") + + self.vars.append(defaults=False, var_list=var_list) + try: - groups_list = list_of_dict_vals(self._read_vars.wanted) + groups_list = list_of_dict_vals(self.vars.wanted) except AttributeError: pass diff --git a/icepyx/core/variables.py b/icepyx/core/variables.py index d46561f46..94645ca94 100644 --- a/icepyx/core/variables.py +++ b/icepyx/core/variables.py @@ -1,9 +1,13 @@ import numpy as np import os import pprint +import warnings from icepyx.core.auth import EarthdataAuthMixin import icepyx.core.is2ref as is2ref +from icepyx.core.exceptions import DeprecationError +import icepyx.core.validate_inputs as val +import icepyx.core as ipxc # DEVGOAL: use h5py to simplify some of these tasks, if possible! @@ -25,11 +29,21 @@ class Variables(EarthdataAuthMixin): contained in ICESat-2 products. Parameters - ---------- + ---------- vartype : string + This argument is deprecated. The vartype will be inferred from data_source. One of ['order', 'file'] to indicate the source of the input variables. This field will be auto-populated when a variable object is created as an attribute of a query object. + path : string, default None + The path to a local Icesat-2 file. The variables list will contain the variables + present in this file. Either path or product are required input arguments. + product : string, default None + Properly formatted string specifying a valid ICESat-2 product. The variables list will + contain all available variables for this product. Either product or path are required + input arguments. + version : string, default None + Properly formatted string specifying a valid version of the ICESat-2 product. avail : dictionary, default None Dictionary (key:values) of available variable names (keys) and paths (values). wanted : dictionary, default None @@ -38,47 +52,72 @@ class Variables(EarthdataAuthMixin): A session object authenticating the user to download data using their Earthdata login information. The session object will automatically be passed from the query object if you have successfully logged in there. - product : string, default None - Properly formatted string specifying a valid ICESat-2 product - version : string, default None - Properly formatted string specifying a valid version of the ICESat-2 product - path : string, default None - For vartype file, a path to a directory of or single input data file (not yet implemented) + """ def __init__( self, - vartype, - avail=None, - wanted=None, + vartype=None, + path=None, product=None, version=None, - path=None, + avail=None, + wanted=None, auth=None, ): - - assert vartype in ["order", "file"], "Please submit a valid variables type flag" + # Deprecation error + if vartype in ['order', 'file']: + raise DeprecationError( + 'It is no longer required to specify the variable type `vartype`. Instead please ', + 'provide either the path to a local file (arg: `path`) or the product you would ', + 'like variables for (arg: `product`).' + ) + + if path and product: + raise TypeError( + 'Please provide either a filepath or a product. If a filepath is provided ', + 'variables will be read from the file. If a product is provided all available ', + 'variables for that product will be returned.' + ) + # Set the product and version from either the input args or the file + if path: + self._path = path + self._product = is2ref.extract_product(self._path) + self._version = is2ref.extract_version(self._path) + elif product: + # Check for valid product string + self._product = is2ref._validate_product(product) + # Check for valid version string + # If version is not specified by the user assume the most recent version + self._version = val.prod_version(is2ref.latest_version(self._product), version) + else: + raise TypeError('Either a filepath or a product need to be given as input arguments.') + # initialize authentication properties EarthdataAuthMixin.__init__(self, auth=auth) - self._vartype = vartype - self.product = product self._avail = avail self.wanted = wanted # DevGoal: put some more/robust checks here to assess validity of inputs - - if self._vartype == "order": - if self._avail == None: - self._version = version - elif self._vartype == "file": - # DevGoal: check that the list or string are valid dir/files - self.path = path - - # @property - # def wanted(self): - # return self._wanted + + @property + def path(self): + if self._path: + path = self._path + else: + path = None + return path + + @property + def product(self): + return self._product + + @property + def version(self): + return self._version + def avail(self, options=False, internal=False): """ @@ -97,16 +136,14 @@ def avail(self, options=False, internal=False): . 'quality_assessment/gt3r/signal_selection_source_fraction_3'] """ - # if hasattr(self, '_avail'): - # return self._avail - # else: + if not hasattr(self, "_avail") or self._avail == None: - if self._vartype == "order": + if not hasattr(self, 'path'): self._avail = is2ref._get_custom_options( - self.session, self.product, self._version + self.session, self.product, self.version )["variables"] - - elif self._vartype == "file": + else: + # If a path was given, use that file to read the variables import h5py self._avail = [] @@ -446,53 +483,14 @@ def append(self, defaults=False, var_list=None, beam_list=None, keyword_list=Non and keyword_list == None ), "You must enter parameters to add to a variable subset list. If you do not want to subset by variable, ensure your is2.subsetparams dictionary does not contain the key 'Coverage'." - req_vars = {} + final_vars = {} - # if not hasattr(self, 'avail') or self.avail==None: self.get_avail() - # vgrp, paths = self.parse_var_list(self.avail) - # allpaths = [] - # [allpaths.extend(np.unique(np.array(paths[p]))) for p in range(len(paths))] vgrp, allpaths = self.avail(options=True, internal=True) - self._check_valid_lists(vgrp, allpaths, var_list, beam_list, keyword_list) - # add the mandatory variables to the data object - if self._vartype == "order": - nec_varlist = [ - "sc_orient", - "sc_orient_time", - "atlas_sdp_gps_epoch", - "data_start_utc", - "data_end_utc", - "granule_start_utc", - "granule_end_utc", - "start_delta_time", - "end_delta_time", - ] - elif self._vartype == "file": - nec_varlist = [ - "sc_orient", - "atlas_sdp_gps_epoch", - "cycle_number", - "rgt", - "data_start_utc", - "data_end_utc", - ] - - # Adjust the nec_varlist for individual products - if self.product == "ATL11": - nec_varlist.remove("sc_orient") - - try: - self._check_valid_lists(vgrp, allpaths, var_list=nec_varlist) - except ValueError: - # Assume gridded product since user input lists were previously validated - nec_varlist = [] - + # Instantiate self.wanted to an empty dictionary if it doesn't exist if not hasattr(self, "wanted") or self.wanted == None: - for varid in nec_varlist: - req_vars[varid] = vgrp[varid] - self.wanted = req_vars + self.wanted = {} # DEVGOAL: add a secondary var list to include uncertainty/error information for lower level data if specific data variables have been specified... @@ -501,21 +499,21 @@ def append(self, defaults=False, var_list=None, beam_list=None, keyword_list=Non # Case only variables (but not keywords or beams) are specified if beam_list == None and keyword_list == None: - req_vars.update(self._iter_vars(sum_varlist, req_vars, vgrp)) + final_vars.update(self._iter_vars(sum_varlist, final_vars, vgrp)) # Case a beam and/or keyword list is specified (with or without variables) else: - req_vars.update( - self._iter_paths(sum_varlist, req_vars, vgrp, beam_list, keyword_list) + final_vars.update( + self._iter_paths(sum_varlist, final_vars, vgrp, beam_list, keyword_list) ) # update the data object variables - for vkey in req_vars.keys(): + for vkey in final_vars.keys(): # add all matching keys and paths for new variables if vkey not in self.wanted.keys(): - self.wanted[vkey] = req_vars[vkey] + self.wanted[vkey] = final_vars[vkey] else: - for vpath in req_vars[vkey]: + for vpath in final_vars[vkey]: if vpath not in self.wanted[vkey]: self.wanted[vkey].append(vpath) From d596c01315fb2de8ab2f8be2e485534961e3f42a Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 6 Nov 2023 17:17:57 -0500 Subject: [PATCH 104/124] use factories as fixture test pattern --- icepyx/tests/test_quest_argo.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index f77f0b70f..93f70a6fe 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -4,15 +4,19 @@ from icepyx.quest.quest import Quest # create an Argo instance via quest (Argo is a submodule) -def argo_quest_instance(bounding_box, date_range): - my_quest = Quest(spatial_extent=bounding_box, date_range=date_range) - my_quest.add_argo() - my_argo = my_quest.datasets["argo"] +@pytest.fixture(scope="function") +def argo_quest_instance(): + def _argo_quest_instance(bounding_box, date_range): # aka "factories as fixtures" + my_quest = Quest(spatial_extent=bounding_box, date_range=date_range) + my_quest.add_argo() + my_argo = my_quest.datasets["argo"] - return my_argo + return my_argo + return _argo_quest_instance -def test_available_profiles(): + +def test_available_profiles(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) obs_msg = reg_a.search_data() @@ -21,7 +25,7 @@ def test_available_profiles(): assert obs_msg == exp_msg -def test_no_available_profiles(): +def test_no_available_profiles(argo_quest_instance): reg_a = argo_quest_instance([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) obs = reg_a.search_data() @@ -32,7 +36,7 @@ def test_no_available_profiles(): assert obs == exp -def test_fmt_coordinates(): +def test_fmt_coordinates(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) obs = reg_a._fmt_coordinates() @@ -41,7 +45,7 @@ def test_fmt_coordinates(): assert obs == exp -def test_invalid_param(): +def test_invalid_param(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) invalid_params = ["temp", "temperature_files"] @@ -56,7 +60,7 @@ def test_invalid_param(): reg_a._validate_parameters(invalid_params) -def test_download_parse_into_df(): +def test_download_parse_into_df(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) reg_a.download(params=["salinity"]) # note: pressure is returned by default @@ -81,7 +85,7 @@ def test_download_parse_into_df(): # then use those for the comparison (e.g. number of rows in df and json match) -def test_merge_df(): +def test_merge_df(argo_quest_instance): reg_a = argo_quest_instance([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) param_list = ["salinity", "temperature", "down_irradiance412"] @@ -97,8 +101,8 @@ def test_merge_df(): assert "down_irradiance412_argoqc" in df.columns -def test_presRange_input_param(): - reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) +def test_presRange_input_param(argo_quest_instance): + reg_a = argo_quest_instance([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) df = reg_a.download(params=["salinity"], presRange="0.2,180") assert df["pressure"].min() >= 0.2 From d7b9424ee9fa9f96b23f1e347749cf39130fe1f8 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 8 Nov 2023 17:20:02 -0500 Subject: [PATCH 105/124] add quest module init file --- icepyx/quest/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 icepyx/quest/__init__.py diff --git a/icepyx/quest/__init__.py b/icepyx/quest/__init__.py new file mode 100644 index 000000000..e69de29bb From 3fb4c48fd52a881933e872553d7101e4f18feb36 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Fri, 17 Nov 2023 13:18:08 -0500 Subject: [PATCH 106/124] streamline params and presRange handling, including docs+tests+ex --- .../QUEST_argo_data_access.ipynb | 89 ++++++++- icepyx/quest/dataset_scripts/argo.py | 133 +++++++++----- icepyx/quest/quest.py | 5 +- icepyx/tests/test_quest_argo.py | 169 +++++++++++++++--- 4 files changed, 324 insertions(+), 72 deletions(-) diff --git a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb index 187529675..21307ebe2 100644 --- a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb +++ b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb @@ -36,6 +36,18 @@ "import icepyx as ipx" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "41bb9895", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "import icepyx as ipx\n", + "%autoreload 2\n" + ] + }, { "cell_type": "markdown", "id": "5c35f5df-b4fb-4a36-8d6f-d20f1552767a", @@ -170,6 +182,71 @@ "reg_a.add_argo()" ] }, + { + "cell_type": "markdown", + "id": "62afb9ad", + "metadata": {}, + "source": [ + "**ZACH**\n", + "\n", + "Could you add a little bit of text around argo parameters/presRange and the ability to search and download multiple times (outside the quest `search_all` and `download_all` options)? A few highlights that come to mind after recent updates:\n", + "- by default only temperature is gotten, but you can supply a list of the parameters you want to `reg_a.add_argo()`\n", + "- you can also directly, at any time, view or update the `reg_a.datasets['argo'].params` value, which will then be used in your next search or download\n", + "- alternatively, you can directly search/download via `reg_a.datasets['argo'].search_data()` and provide `params` or `presRange` keyword arguments that will replace the existing values of `reg_a.datasets['argo'].params`/`reg_a.datasets['argo'].presRange`\n", + "- when downloading, you can also provide the `keep_existing=True` kwarg to add more profiles, parameters, pressure ranges to your existing dataframe (and have them merged nicely for you)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc921ca5", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "435a1243", + "metadata": {}, + "outputs": [], + "source": [ + "# see what argo parameters will be searched for or downloaded\n", + "reg_a.datasets['argo'].params" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e34756b8", + "metadata": {}, + "outputs": [], + "source": [ + "# update the list of argo parameters\n", + "reg_a.datasets['argo'].params = ['temperature','salinity']\n", + "\n", + "# if you submit an invalid parameter (such as 'temp' instead of 'temperature') you'll get an \n", + "# AssertionError and message saying the parameter is invalid (example: reg_a.datasets['argo'].params = ['temp','salinity'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c15675df", + "metadata": {}, + "outputs": [], + "source": [ + "reg_a.datasets['argo'].search_data()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db56cc33", + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "id": "70d36566-0d3c-4781-a199-09bb11dad975", @@ -204,11 +281,13 @@ "user_expressions": [] }, "source": [ - "If the code worked correctly, then there should be 19 available Argo profiles, each containing `temperature` and `pressure`, compiled into a Pandas DataFrame. When BGC Argo is fully implemented to QUEST, we could add more variables to this list.\n", + "We now have 19 available Argo profiles, each containing `temperature` and `pressure`, compiled into a Pandas DataFrame. **NOTE: BGC Argo is currently fully implemented** When BGC Argo is fully implemented to QUEST, we could add more variables to this list.\n", "\n", "We also have a series of files containing ICESat-2 ATL03 data. Because these data files are very large, we are only going to focus on one of these files for this example.\n", "\n", - "Let's now load one of the ICESat-2 files and see where it passes relative to the Argo float data." + "Let's now load one of the ICESat-2 files and see where it passes relative to the Argo float data.\n", + "\n", + "**Zach** would you be open to switching this to use icepyx's read module? We could easily use the `xarray.to_dataframe` to then work with the rest of this notebook!" ] }, { @@ -392,9 +471,9 @@ ], "metadata": { "kernelspec": { - "display_name": "base", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "base" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -406,7 +485,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.10" } }, "nbformat": 4, diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 5b53ca1e8..dfe0f692f 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -11,6 +11,21 @@ class Argo(DataSet): Initialises an Argo Dataset object via a Quest object. Used to query physical and BGC Argo profiles. + Parameters + --------- + aoi: + area of interest supplied via the spatial parameter of the QUEST object + toi: + time period of interest supplied via the temporal parameter of the QUEST object + params: list of str, default ["temperature"] + A list of strings, where each string is a requested parameter. + Only metadata for profiles with the requested parameters are returned. + To search for all parameters, use `params=["all"]`; + be careful using all for floats with BGC data, as this may be result in a large download. + presRange: str, default None + The pressure range (which correllates with depth) to search for data within. + Input as a "shallow-limit,deep-limit" string. + See Also -------- DataSet @@ -19,8 +34,8 @@ class Argo(DataSet): # Note: it looks like ArgoVis now accepts polygons, not just bounding boxes def __init__(self, aoi, toi, params=["temperature"], presRange=None): # super().__init__(boundingbox, timeframe) - self.params = self._validate_parameters(params) - self.presRange = presRange + self._params = self._validate_parameters(params) + self._presRange = presRange self._spatial = aoi self._temporal = toi # todo: verify that this will only work with a bounding box (I think our code can accept arbitrary polygons) @@ -48,6 +63,44 @@ def __str__(self): return s + # ---------------------------------------------------------------------- + # Properties + + @property + def params(self) -> list: + """ + User's list of Argo parameters to search (query) and download. + + The user may modify this list directly. + """ + + return self._params + + @params.setter + def params(self, value): + """ + Validate the input list of parameters. + """ + self._params = list(set(self._validate_parameters(value))) + + @property + def presRange(self) -> str: + """ + User's pressure range to search (query) and download. + + The user may modify this string directly. + """ + + return self._presRange + + @presRange.setter + def presRange(self, value): + """ + Update the presRange based on the user input + """ + + self._presRange = value + # ---------------------------------------------------------------------- # Formatting API Inputs @@ -177,26 +230,33 @@ def _validate_parameters(self, params) -> list: i, valid_params ) - return params + return list(set(params)) # ---------------------------------------------------------------------- # Querying and Getting Data - def search_data(self, params=None, printURL=False) -> str: + def search_data(self, params=None, presRange=None, printURL=False) -> str: """ Query for available argo profiles given the spatio temporal criteria and other params specific to the dataset. + Searches will automatically use the parameter and pressure range inputs + supplied when the `quest.argo` object was created unless replacement arguments + are added here. Parameters --------- - params: list of str, default ["temperature"] + params: list of str, default None A list of strings, where each string is a requested parameter. + This kwarg is used to replace the existing list in `self.params`. + Do not submit this kwarg if you would like to use the existing `self.params` list. Only metadata for profiles with the requested parameters are returned. To search for all parameters, use `params=["all"]`; be careful using all for floats with BGC data, as this may be result in a large download. presRange: str, default None The pressure range (which correllates with depth) to search for data within. - Input as a "shallow-limit,deep-limit" string. Note the lack of space. + This kwarg is used to replace the existing pressure range in `self.presRange`. + Do not submit this kwarg if you would like to use the existing `self.presRange` values. + Input as a "shallow-limit,deep-limit" string. printURL: boolean, default False Print the URL of the data request. Useful for debugging and when no data is returned. @@ -205,11 +265,12 @@ def search_data(self, params=None, printURL=False) -> str: str: message on the success status of the search """ - # if new search is called with additional parameters + # if search is called with replaced parameters or presRange if not params is None: - self.params.extend(self._validate_parameters(params)) - # to remove duplicated from list - self.params = list(set(self.params)) + self.params = params + + if not presRange is None: + self.presRange = presRange # builds URL to be submitted baseURL = "https://argovis-api.colorado.edu/argo" @@ -219,7 +280,8 @@ def search_data(self, params=None, printURL=False) -> str: "polygon": [self._fmt_coordinates()], "data": self.params, } - if self.presRange: + + if self.presRange is not None: payload["presRange"] = self.presRange # submit request @@ -252,6 +314,7 @@ def search_data(self, params=None, printURL=False) -> str: prof_ids = [] for i in selectionProfiles: prof_ids.append(i["_id"]) + # should we be doing a set/duplicates check here?? self.prof_ids = prof_ids msg = "{0} valid profiles have been identified".format(len(prof_ids)) @@ -261,8 +324,6 @@ def search_data(self, params=None, printURL=False) -> str: def _download_profile( self, profile_number, - params=None, - presRange=None, printURL=False, ) -> dict: """ @@ -272,13 +333,6 @@ def _download_profile( --------- profile_number: str String containing the argo profile ID of the data being downloaded. - params: list of str, default None - A list of strings, where each string is a requested parameter. - Only data for the requested parameters are returned. - To download all parameters, use `params=["all"]`. - presRange: str, default None - The pressure range (which correllates with depth) to download data within. - Input as a "shallow-limit,deep-limit" string. Note the lack of space. printURL: boolean, default False Print the URL of the data request. Useful for debugging and when no data is returned. @@ -291,11 +345,11 @@ def _download_profile( baseURL = "https://argovis-api.colorado.edu/argo" payload = { "id": profile_number, - "data": params, + "data": self.params, } - if presRange: - payload["presRange"] = presRange + if self.presRange: + payload["presRange"] = self.presRange # submit request resp = requests.get( @@ -355,18 +409,24 @@ def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFr Downloads the requested data for a list of profile IDs (stored under .prof_ids) and returns it in a DataFrame. Data is also stored in self.argodata. + Note that if new inputs (`params` or `presRange`) are supplied and `keep_existing=True`, + the existing data will not be limited to the new input parameters. Parameters ---------- - params: list of str, default ["temperature", "pressure] + params: list of str, default None A list of strings, where each string is a requested parameter. + This kwarg is used to replace the existing list in `self.params`. + Do not submit this kwarg if you would like to use the existing `self.params` list. Only metadata for profiles with the requested parameters are returned. To search for all parameters, use `params=["all"]`. For a list of available parameters, see: `reg._valid_params` presRange: str, default None The pressure range (which correllates with depth) to search for data within. - Input as a "shallow-limit,deep-limit" string. Note the lack of space. - keep_existing: Boolean, default True + This kwarg is used to replace the existing pressure range in `self.presRange`. + Do not submit this kwarg if you would like to use the existing `self.presRange` values. + Input as a "shallow-limit,deep-limit" string. + keep_existing: boolean, default True Provides the option to clear any existing downloaded data before downloading more. Returns @@ -387,27 +447,22 @@ def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFr "will be added to previously downloaded data.", ) - # if new search is called with additional parameters + # if download is called with replaced parameters or presRange if not params is None: - self.params.extend(self._validate_parameters(params)) - # to remove duplicated from list - self.params = list(set(self.params)) - else: - params = self.params + self.params = params - # if new search is called with new pressure range if not presRange is None: self.presRange = presRange # Add qc data for each of the parameters requested - if params == ["all"]: + if self.params == ["all"]: pass else: - for p in params: - if p.endswith("_argoqc") or (p + "_argoqc" in params): + for p in self.params: + if p.endswith("_argoqc") or (p + "_argoqc" in self.params): pass else: - params.append(p + "_argoqc") + self.params.append(p + "_argoqc") # intentionally resubmit search to reset prof_ids, in case the user requested different parameters self.search_data() @@ -416,9 +471,7 @@ def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFr merged_df = pd.DataFrame(columns=["profile_id"]) for i in self.prof_ids: print("processing profile", i) - profile_data = self._download_profile( - i, params=params, presRange=presRange, printURL=True - ) + profile_data = self._download_profile(i) profile_df = self._parse_into_df(profile_data[0]) merged_df = pd.concat([merged_df, profile_df], sort=False) diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index 465bbcfcf..980512104 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -20,7 +20,7 @@ class Quest(GenQuery): proj : proj4 string Geospatial projection. Not yet implemented - + Returns ------- @@ -102,7 +102,6 @@ def add_icesat2( Parameters ---------- - For details on inputs, see the Query documentation. Returns @@ -130,7 +129,6 @@ def add_icesat2( self.datasets["icesat2"] = query - def add_argo(self, params=["temperature"], presRange=None) -> None: """ Adds Argo (including Argo-BGC) to QUEST structure. @@ -158,7 +156,6 @@ def add_argo(self, params=["temperature"], presRange=None) -> None: argo = Argo(self._spatial, self._temporal, params, presRange) self.datasets["argo"] = argo - # ---------------------------------------------------------------------- # Methods (on all datasets) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index 93f70a6fe..92a18e933 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -16,39 +16,55 @@ def _argo_quest_instance(bounding_box, date_range): # aka "factories as fixture return _argo_quest_instance -def test_available_profiles(argo_quest_instance): +# --------------------------------------------------- +# Test Formatting and Validation + + +def test_fmt_coordinates(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) - obs_msg = reg_a.search_data() + obs = reg_a._fmt_coordinates() - exp_msg = "19 valid profiles have been identified" + exp = "[[-143.0,30.0],[-143.0,37.0],[-154.0,37.0],[-154.0,30.0],[-143.0,30.0]]" - assert obs_msg == exp_msg + assert obs == exp -def test_no_available_profiles(argo_quest_instance): - reg_a = argo_quest_instance([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) - obs = reg_a.search_data() +def test_validate_parameters(argo_quest_instance): + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) - exp = ( - "Warning: Query returned no profiles\n" "Please try different search parameters" + invalid_params = ["temp", "temperature_files"] + + ermsg = re.escape( + "Parameter '{0}' is not valid. Valid parameters are {1}".format( + "temp", reg_a._valid_params() + ) ) - assert obs == exp + with pytest.raises(AssertionError, match=ermsg): + reg_a._validate_parameters(invalid_params) -def test_fmt_coordinates(argo_quest_instance): +# --------------------------------------------------- +# Test Setters + + +def test_param_setter(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) - obs = reg_a._fmt_coordinates() - exp = "[[-143.0,30.0],[-143.0,37.0],[-154.0,37.0],[-154.0,30.0],[-143.0,30.0]]" + exp = ["temperature"] + assert reg_a.params == exp - assert obs == exp + reg_a.params = ["temperature", "salinity"] + + exp = list(set(["temperature", "salinity"])) + assert reg_a.params == exp -def test_invalid_param(argo_quest_instance): +def test_param_setter_invalid_inputs(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) - invalid_params = ["temp", "temperature_files"] + exp = ["temperature"] + assert reg_a.params == exp ermsg = re.escape( "Parameter '{0}' is not valid. Valid parameters are {1}".format( @@ -57,18 +73,76 @@ def test_invalid_param(argo_quest_instance): ) with pytest.raises(AssertionError, match=ermsg): - reg_a._validate_parameters(invalid_params) + reg_a.params = ["temp", "salinity"] + + +def test_presRange_setter(argo_quest_instance): + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + + exp = None + assert reg_a.presRange == exp + + reg_a.presRange = "0.5,150" + + exp = "0.5,150" + assert reg_a.presRange == exp + + +def test_presRange_setter_invalid_inputs(argo_quest_instance): + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + + exp = None + assert reg_a.presRange == exp + + reg_a.presRange = ( + "0.5, sam" # it looks like the API will take a string with a space + ) + + # this setter doesn't currently have a validation check, so would need to search + obs_msg = reg_a.search_data() + + exp_msg = "Error: Unexpected response " + + assert obs_msg == exp_msg + + +# --------------------------------------------------- +# Test search_data + + +def test_search_data_available_profiles(argo_quest_instance): + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + obs_msg = reg_a.search_data() + + exp_msg = "19 valid profiles have been identified" + + assert obs_msg == exp_msg + + +def test_search_data_no_available_profiles(argo_quest_instance): + reg_a = argo_quest_instance([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) + obs = reg_a.search_data() + + exp = ( + "Warning: Query returned no profiles\n" "Please try different search parameters" + ) + + assert obs == exp + + +# --------------------------------------------------- +# Test download and df def test_download_parse_into_df(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) - reg_a.download(params=["salinity"]) # note: pressure is returned by default + reg_a.download() # note: pressure is returned by default obs_cols = reg_a.argodata.columns exp_cols = [ - "salinity", - "salinity_argoqc", + "temperature", + "temperature_argoqc", "pressure", "profile_id", "lat", @@ -78,7 +152,7 @@ def test_download_parse_into_df(argo_quest_instance): assert set(exp_cols) == set(obs_cols) - assert len(reg_a.argodata) == 1942 + assert len(reg_a.argodata) == 2948 # approach for additional testing of df functions: create json files with profiles and store them in test suite @@ -101,10 +175,59 @@ def test_merge_df(argo_quest_instance): assert "down_irradiance412_argoqc" in df.columns -def test_presRange_input_param(argo_quest_instance): - reg_a = argo_quest_instance([-55, 68, -48, 71], ["2019-02-20", "2019-02-28"]) +# --------------------------------------------------- +# Test kwargs to replace params and presRange in search and download + + +def test_replace_param_search(argo_quest_instance): + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + + obs = reg_a.search_data(params=["doxy"]) + + exp = ( + "Warning: Query returned no profiles\n" "Please try different search parameters" + ) + + assert obs == exp + + +def test_replace_param_download(argo_quest_instance): + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) + reg_a.download(params=["salinity"]) # note: pressure is returned by default + + obs_cols = reg_a.argodata.columns + + exp_cols = [ + "salinity", + "salinity_argoqc", + "pressure", + "profile_id", + "lat", + "lon", + "date", + ] + + assert set(exp_cols) == set(obs_cols) + + assert len(reg_a.argodata) == 1942 + + +def test_replace_presRange_search(argo_quest_instance): + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) + obs_msg = reg_a.search_data(presRange="100,600") + + exp_msg = "19 valid profiles have been identified" + + assert obs_msg == exp_msg + + +def test_replace_presRange_download(argo_quest_instance): + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) df = reg_a.download(params=["salinity"], presRange="0.2,180") assert df["pressure"].min() >= 0.2 assert df["pressure"].max() <= 180 assert "salinity" in df.columns + + +# second pres range test where does have a higher max pressure because only the new data was presRange limited? From 283fc04bfb722a9c15bd1d27eb0d6869840c89e4 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Tue, 21 Nov 2023 10:03:18 -0500 Subject: [PATCH 107/124] fix failing test due to list order --- icepyx/tests/test_quest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/icepyx/tests/test_quest.py b/icepyx/tests/test_quest.py index 4de8a26ca..8746718a8 100644 --- a/icepyx/tests/test_quest.py +++ b/icepyx/tests/test_quest.py @@ -43,7 +43,7 @@ def test_add_argo(quest_instance): assert type(obs) == dict assert exp_key in obs.keys() assert type(obs[exp_key]) == exp_type - assert quest_instance.datasets[exp_key].params == params + assert set(quest_instance.datasets[exp_key].params) == set(params) def test_add_multiple_datasets(quest_instance): @@ -72,8 +72,8 @@ def test_search_all(quest_instance): "kwargs", [ {"icesat2": {"IDs": True}}, - {"argo":{"presRange":"10,500"}}, - {"icesat2":{"IDs":True}, "argo":{"presRange":"10,500"}} + {"argo": {"presRange": "10,500"}}, + {"icesat2": {"IDs": True}, "argo": {"presRange": "10,500"}}, ], ) def test_search_all_kwargs(quest_instance, kwargs): From d888127f22efcaade481c7612eeba144164eb506 Mon Sep 17 00:00:00 2001 From: zachghiaccio Date: Mon, 27 Nov 2023 17:32:39 +0000 Subject: [PATCH 108/124] Addressed Jessica's suggestions for QUEST notebook. --- .../QUEST_argo_data_access.ipynb | 239 +++++++++++++----- 1 file changed, 174 insertions(+), 65 deletions(-) diff --git a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb index 21307ebe2..4c6d71ffc 100644 --- a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb +++ b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb @@ -36,18 +36,6 @@ "import icepyx as ipx" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "41bb9895", - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "import icepyx as ipx\n", - "%autoreload 2\n" - ] - }, { "cell_type": "markdown", "id": "5c35f5df-b4fb-4a36-8d6f-d20f1552767a", @@ -184,35 +172,35 @@ }, { "cell_type": "markdown", - "id": "62afb9ad", - "metadata": {}, + "id": "7bade19e-5939-410a-ad54-363636289082", + "metadata": { + "user_expressions": [] + }, "source": [ - "**ZACH**\n", - "\n", - "Could you add a little bit of text around argo parameters/presRange and the ability to search and download multiple times (outside the quest `search_all` and `download_all` options)? A few highlights that come to mind after recent updates:\n", - "- by default only temperature is gotten, but you can supply a list of the parameters you want to `reg_a.add_argo()`\n", - "- you can also directly, at any time, view or update the `reg_a.datasets['argo'].params` value, which will then be used in your next search or download\n", - "- alternatively, you can directly search/download via `reg_a.datasets['argo'].search_data()` and provide `params` or `presRange` keyword arguments that will replace the existing values of `reg_a.datasets['argo'].params`/`reg_a.datasets['argo'].presRange`\n", - "- when downloading, you can also provide the `keep_existing=True` kwarg to add more profiles, parameters, pressure ranges to your existing dataframe (and have them merged nicely for you)" + "When accessing Argo data, the variables of interest will be organized as vertical profiles as a function of pressure. By default, only temperature is queried, but the user can supply a list of desired parameters using the code below." ] }, { "cell_type": "code", "execution_count": null, - "id": "dc921ca5", - "metadata": {}, + "id": "6739c3aa-1a88-4d8e-9fd8-479528c20e97", + "metadata": { + "tags": [] + }, "outputs": [], - "source": [] + "source": [ + "# Customized variable query\n", + "reg_a.add_argo(params=['temperature'])" + ] }, { - "cell_type": "code", - "execution_count": null, - "id": "435a1243", - "metadata": {}, - "outputs": [], + "cell_type": "markdown", + "id": "2d06436c-2271-4229-8196-9f5180975ab1", + "metadata": { + "user_expressions": [] + }, "source": [ - "# see what argo parameters will be searched for or downloaded\n", - "reg_a.datasets['argo'].params" + "Additionally, a user may view or update the list of Argo parameters at any time through `reg_a.datasets['argo'].params`. If a user submits an invalid parameter (\"temp\" instead of \"temperature\", for example), an `AssertionError` will be passed." ] }, { @@ -223,29 +211,49 @@ "outputs": [], "source": [ "# update the list of argo parameters\n", - "reg_a.datasets['argo'].params = ['temperature','salinity']\n", - "\n", - "# if you submit an invalid parameter (such as 'temp' instead of 'temperature') you'll get an \n", - "# AssertionError and message saying the parameter is invalid (example: reg_a.datasets['argo'].params = ['temp','salinity'])" + "reg_a.datasets['argo'].params = ['temperature','salinity']" + ] + }, + { + "cell_type": "markdown", + "id": "453900c1-cd62-40c9-820c-0615f63f17f5", + "metadata": { + "user_expressions": [] + }, + "source": [ + "Another approach to directly search or download Argo data is to use `reg_a.datasets['argo'].search_data()`, and `reg_a.datasets['argo'].download()` as long as specific parameters and pressure ranges are given to `params` and `presRange`, respectively." + ] + }, + { + "cell_type": "markdown", + "id": "3f55be4e-d261-49c1-ac14-e19d8e0ff828", + "metadata": { + "user_expressions": [] + }, + "source": [ + "With our current setup, let's see what Argo parameters we will get." ] }, { "cell_type": "code", "execution_count": null, - "id": "c15675df", + "id": "435a1243", "metadata": {}, "outputs": [], "source": [ - "reg_a.datasets['argo'].search_data()" + "# see what argo parameters will be searched for or downloaded\n", + "reg_a.datasets['argo'].params" ] }, { "cell_type": "code", "execution_count": null, - "id": "db56cc33", + "id": "c15675df", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "reg_a.datasets['argo'].search_data()" + ] }, { "cell_type": "markdown", @@ -271,23 +279,109 @@ "path = '/icepyx/quest/downloaded-data/'\n", "\n", "# Access Argo and ICESat-2 data simultaneously\n", - "reg_a.download_all(path)" + "reg_a.download_all()" ] }, { "cell_type": "markdown", - "id": "6970f0ad-9364-4732-a5e6-f93cf3fc31a3", + "id": "ad29285e-d161-46ea-8a57-95891fa2b237", "metadata": { + "tags": [], "user_expressions": [] }, "source": [ - "We now have 19 available Argo profiles, each containing `temperature` and `pressure`, compiled into a Pandas DataFrame. **NOTE: BGC Argo is currently fully implemented** When BGC Argo is fully implemented to QUEST, we could add more variables to this list.\n", - "\n", - "We also have a series of files containing ICESat-2 ATL03 data. Because these data files are very large, we are only going to focus on one of these files for this example.\n", + "We now have 19 available Argo profiles, each containing `temperature` and `pressure`, compiled into a Pandas DataFrame. BGC Argo is also available through QUEST, so we could add more variables to this list.\n", "\n", - "Let's now load one of the ICESat-2 files and see where it passes relative to the Argo float data.\n", + "If the user wishes to add more profiles, parameters, and/or pressure ranges to a pre-existing DataFrame, then they should use `reg_a.download_all(path, keep_existing=True)` to retain previously queried data." + ] + }, + { + "cell_type": "markdown", + "id": "6970f0ad-9364-4732-a5e6-f93cf3fc31a3", + "metadata": { + "user_expressions": [] + }, + "source": [ + "The download function also provided a series of files containing ICESat-2 ATL03 data. Because these data files are very large, we are only going to focus on one file for this example.\n", "\n", - "**Zach** would you be open to switching this to use icepyx's read module? We could easily use the `xarray.to_dataframe` to then work with the rest of this notebook!" + "The below workflow uses the icepyx Read module to quickly load ICESat-2 data into the XArray format." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88f4b1b0-8c58-414c-b6a8-ce1662979943", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "#path_root = '/icepyx/quest-test-data/'\n", + "path_root = '/icepyx/quest-test-data/processed_ATL03_20220419002753_04111506_006_02.h5'\n", + "reader = ipx.Read(path_root)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "665d79a7-7360-4846-99c2-222b34df2a92", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "reader.vars.append(beam_list=['gt2l'], \n", + " var_list=['h_ph', \"lat_ph\", \"lon_ph\", 'signal_conf_ph'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e7158814-50f0-4940-980c-9bb800360982", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "ds = reader.load()\n", + "ds" + ] + }, + { + "cell_type": "markdown", + "id": "1040438c-d806-4964-b4f0-1247da9f3f1f", + "metadata": { + "user_expressions": [] + }, + "source": [ + "To make the data more easily plottable, let's convert the data into a Pandas DataFrame. Note that this method is memory-intensive for ATL03 data, so users are suggested to look at small spatial domains to prevent the notebook from crashing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc086db7-f5a1-4ba7-ba90-5b19afaf6808", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "is2_pd = ds.to_dataframe()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc67e039-338c-4348-acaf-96f605cf0030", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Rearrange the data to only include \"ocean\" photons\n", + "is2_pd = is2_pd.reset_index(level=[0,1,2])\n", + "is2_pd_ocean = is2_pd[is2_pd.index==1]\n", + "is2_pd_ocean" ] }, { @@ -299,14 +393,6 @@ }, "outputs": [], "source": [ - "# Load ICESat-2 latitudes, longitudes, heights, and photon confidence (optional)\n", - "is2_pd = pd.DataFrame()\n", - "with h5py.File(f'{path_root}processed_ATL03_20220419002753_04111506_006_02.h5', 'r') as f:\n", - " is2_pd['lat'] = f['gt2l/heights/lat_ph'][:]\n", - " is2_pd['lon'] = f['gt2l/heights/lon_ph'][:]\n", - " is2_pd['height'] = f['gt2l/heights/h_ph'][:]\n", - " is2_pd['signal_conf'] = f['gt2l/heights/signal_conf_ph'][:,1]\n", - " \n", "# Set Argo data as its own DataFrame\n", "argo_df = reg_a.datasets['argo'].argodata" ] @@ -321,8 +407,8 @@ "outputs": [], "source": [ "# Convert both DataFrames into GeoDataFrames\n", - "is2_gdf = gpd.GeoDataFrame(is2_pd, \n", - " geometry=gpd.points_from_xy(is2_pd.lon, is2_pd.lat),\n", + "is2_gdf = gpd.GeoDataFrame(is2_pd_ocean, \n", + " geometry=gpd.points_from_xy(is2_pd_ocean['lon_ph'], is2_pd_ocean['lat_ph']),\n", " crs='EPSG:4326'\n", ")\n", "argo_gdf = gpd.GeoDataFrame(argo_df, \n", @@ -338,7 +424,22 @@ "user_expressions": [] }, "source": [ - "To view the relative locations of ICESat-2 and Argo, the below cell uses the `explore()` function from GeoPandas. For large datasets like ICESat-2, loading the map might take a while." + "To view the relative locations of ICESat-2 and Argo, the below cell uses the `explore()` function from GeoPandas. The time variables cause errors in the function, so we will drop those variables first. \n", + "\n", + "Note that for large datasets like ICESat-2, loading the map might take a while." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7178fecc-6ca1-42a1-98d4-08f57c050daa", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Drop time variables that would cause errors in explore() function\n", + "is2_gdf = is2_gdf.drop(['data_start_utc','data_end_utc','delta_time','atlas_sdp_gps_epoch'], axis=1)" ] }, { @@ -351,8 +452,8 @@ "outputs": [], "source": [ "# Plot ICESat-2 track (medium/high confidence photons only) on a map\n", - "m = is2_gdf[is2_gdf['signal_conf']>=3].explore(tiles='Esri.WorldImagery',\n", - " name='ICESat-2')\n", + "m = is2_gdf[is2_gdf['signal_conf_ph']>=3].explore(column='rgt', tiles='Esri.WorldImagery',\n", + " name='ICESat-2')\n", "\n", "# Add Argo float locations to map\n", "argo_gdf.explore(m=m, name='Argo', marker_kwds={\"radius\": 6}, color='red')" @@ -408,7 +509,7 @@ "outputs": [], "source": [ "# Only consider ICESat-2 signal photons\n", - "is2_pd_signal = is2_pd[is2_pd['signal_conf']>0]\n", + "is2_pd_signal = is2_pd_ocean[is2_pd_ocean['signal_conf_ph']>=0]\n", "\n", "## Multi-panel plot showing ICESat-2 and Argo data\n", "\n", @@ -425,7 +526,7 @@ "world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))\n", "world.plot(ax=ax1, color='0.8', edgecolor='black')\n", "argo_df.plot.scatter(ax=ax1, x='lon', y='lat', s=25.0, c='green', zorder=3, alpha=0.3)\n", - "is2_pd.plot.scatter(ax=ax1, x='lon', y='lat', s=10.0, zorder=2, alpha=0.3)\n", + "is2_pd_signal.plot.scatter(ax=ax1, x='lon_ph', y='lat_ph', s=10.0, zorder=2, alpha=0.3)\n", "ax1.plot(lons, lats, linewidth=1.5, color='orange', zorder=2)\n", "#df.plot(ax=ax2, x='lon', y='lat', marker='o', color='red', markersize=2.5, zorder=3)\n", "ax1.set_xlim(-160,-100)\n", @@ -436,7 +537,7 @@ "\n", "# Plot Zoomed View of Ground Tracks\n", "argo_df.plot.scatter(ax=ax2, x='lon', y='lat', s=50.0, c='green', zorder=3, alpha=0.3)\n", - "is2_pd.plot.scatter(ax=ax2, x='lon', y='lat', s=10.0, zorder=2, alpha=0.3)\n", + "is2_pd_signal.plot.scatter(ax=ax2, x='lon_ph', y='lat_ph', s=10.0, zorder=2, alpha=0.3)\n", "ax2.plot(lons, lats, linewidth=1.5, color='orange', zorder=1)\n", "ax2.scatter(-151.98956, 34.43885, color='orange', marker='^', s=80, zorder=4)\n", "ax2.set_xlim(min(lons) - lon_margin, max(lons) + lon_margin)\n", @@ -446,10 +547,10 @@ "ax2.set_ylabel('Latitude', fontsize=18)\n", "\n", "# Plot ICESat-2 along-track vertical profile. A dotted line notes the location of a nearby Argo float\n", - "is2 = ax3.scatter(is2_pd_signal['lat'], is2_pd_signal['height'], s=0.1)\n", + "is2 = ax3.scatter(is2_pd_signal['lat_ph'], is2_pd_signal['h_ph']+13.1, s=0.1)\n", "ax3.axvline(34.43885, linestyle='--', linewidth=3, color='black')\n", "ax3.set_xlim([34.3, 34.5])\n", - "ax3.set_ylim([-15, 5])\n", + "ax3.set_ylim([-20, 5])\n", "ax3.set_xlabel('Latitude', fontsize=18)\n", "ax3.set_ylabel('Approx. IS-2 Depth [m]', fontsize=16)\n", "ax3.set_yticklabels(['15', '10', '5', '0', '-5'])\n", @@ -467,6 +568,14 @@ "# Save figure\n", "#plt.savefig('/icepyx/quest/figures/is2_argo_figure.png', dpi=500)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b6548e2-0662-4c8b-a251-55ca63aff99b", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -485,7 +594,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.10" + "version": "3.10.12" } }, "nbformat": 4, From afc7edd4d640f1860294e8ee6ee0873ad5482721 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 27 Nov 2023 15:09:34 -0500 Subject: [PATCH 109/124] remove duplicate QUEST page in docs --- doc/source/index.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 1d74073bc..612af6adc 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -146,10 +146,9 @@ ICESat-2 datasets to enable scientific discovery. contributing/contributors_link contributing/contribution_guidelines contributing/how_to_contribute - contributing/quest-available-datasets + contributing/attribution_link contributing/icepyx_internals contributing/quest-available-datasets - contributing/attribution_link contributing/development_plan contributing/release_guide contributing/code_of_conduct_link From ab4032834a99e44d43b2908b6cb374d6819d626c Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 27 Nov 2023 16:24:19 -0500 Subject: [PATCH 110/124] add some text to QUEST example notebook --- .../QUEST_argo_data_access.ipynb | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb index 4c6d71ffc..f8a66fe7f 100644 --- a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb +++ b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb @@ -9,7 +9,7 @@ "source": [ "# QUEST Example: Finding Argo and ICESat-2 data\n", "\n", - "In this notebook, we are going to find Argo and ICESat-2 data over a region of the Pacific Ocean. Normally, we would require multiple data portals or Python packages to accomplish this. However, thanks to the QUEST module, we can use icepyx to find both!" + "In this notebook, we are going to find Argo and ICESat-2 data over a region of the Pacific Ocean. Normally, we would require multiple data portals or Python packages to accomplish this. However, thanks to the [QUEST (Query, Unify, Explore SpatioTemporal) module](https://icepyx.readthedocs.io/en/latest/contributing/quest-available-datasets.html), we can use icepyx to find both!" ] }, { @@ -65,7 +65,7 @@ "spatial_extent = [-154, 30, -143, 37]\n", "\n", "# Start and end dates, in YYYY-MM-DD format\n", - "date_range = ['2022-04-12', '2022-04-26']\n", + "date_range = ['2022-04-12', '2022-04-14']\n", "\n", "# Initialize the QUEST object\n", "reg_a = ipx.Quest(spatial_extent=spatial_extent, date_range=date_range)\n", @@ -142,9 +142,9 @@ "user_expressions": [] }, "source": [ - "Note that many of the ICESat-2 functions shown here are the same as those used for normal icepyx queries. The user is referred to other example workbooks for detailed explanations about additional icepyx features.\n", + "Note the ICESat-2 functions shown here are the same as those used for direct icepyx queries. The user is referred to other example workbooks for detailed explanations about additional icepyx features.\n", "\n", - "Downloading ICESat-2 data requires Earthdata login credentials. When running the `download_all()` function below, an authentication check will be passed when attempting to download the ICESat-2 files." + "Accessing ICESat-2 data requires Earthdata login credentials. When running the `download_all()` function below, an authentication check will be passed when attempting to download the ICESat-2 files." ] }, { @@ -177,7 +177,9 @@ "user_expressions": [] }, "source": [ - "When accessing Argo data, the variables of interest will be organized as vertical profiles as a function of pressure. By default, only temperature is queried, but the user can supply a list of desired parameters using the code below." + "When accessing Argo data, the variables of interest will be organized as vertical profiles as a function of pressure. By default, only temperature is queried, so the user should supply a list of desired parameters using the code below. The user may also limit the pressure range of the returned data by passing `presRange=\"0,200\"`.\n", + "\n", + "*Note: Our example shows only physical Argo float parameters, but the process is identical for including BGC float parameters.*" ] }, { @@ -189,8 +191,8 @@ }, "outputs": [], "source": [ - "# Customized variable query\n", - "reg_a.add_argo(params=['temperature'])" + "# Customized variable query to retrieve salinity instead of temperature\n", + "reg_a.add_argo(params=['salinity'])" ] }, { @@ -200,7 +202,7 @@ "user_expressions": [] }, "source": [ - "Additionally, a user may view or update the list of Argo parameters at any time through `reg_a.datasets['argo'].params`. If a user submits an invalid parameter (\"temp\" instead of \"temperature\", for example), an `AssertionError` will be passed." + "Additionally, a user may view or update the list of requested Argo and Argo-BGC parameters at any time through `reg_a.datasets['argo'].params`. If a user submits an invalid parameter (\"temp\" instead of \"temperature\", for example), an `AssertionError` will be passed. `reg_a.datasets['argo'].presRange` behaves anologously for limiting the pressure range of Argo data." ] }, { @@ -211,7 +213,10 @@ "outputs": [], "source": [ "# update the list of argo parameters\n", - "reg_a.datasets['argo'].params = ['temperature','salinity']" + "reg_a.datasets['argo'].params = ['temperature','salinity']\n", + "\n", + "# show the current list\n", + "reg_a.datasets['argo'].params" ] }, { @@ -221,7 +226,9 @@ "user_expressions": [] }, "source": [ - "Another approach to directly search or download Argo data is to use `reg_a.datasets['argo'].search_data()`, and `reg_a.datasets['argo'].download()` as long as specific parameters and pressure ranges are given to `params` and `presRange`, respectively." + "As for ICESat-2 data, the user can interact directly with the Argo data object (`reg_a.datasets['argo']`) to directly search or download data outside of the `Quest.search_all()` and `Quest.download_all()` functionality shown below.\n", + "\n", + "The approach to directly search or download Argo data is to use `reg_a.datasets['argo'].search_data()`, and `reg_a.datasets['argo'].download()`. In both cases, the existing parameters and pressure ranges are used unless the user passes new `params` and/or `presRange` kwargs, respectively, which will directly update those values (stored attributes)." ] }, { @@ -264,7 +271,7 @@ "source": [ "Now we can access the data for both Argo and ICESat-2! The below function will do this for us.\n", "\n", - "**Important**: With our current code, the Argo data will be compiled into a Pandas DataFrame, which must be manually saved by the user. The ICESat-2 data is saved as processed HDF-5 files to the directory given below." + "**Important**: The Argo data will be compiled into a Pandas DataFrame, which must be manually saved by the user. The ICESat-2 data is saved as processed HDF-5 files to the directory given below." ] }, { @@ -276,10 +283,10 @@ }, "outputs": [], "source": [ - "path = '/icepyx/quest/downloaded-data/'\n", + "path = './quest/downloaded-data/'\n", "\n", "# Access Argo and ICESat-2 data simultaneously\n", - "reg_a.download_all()" + "reg_a.download_all(path=path)" ] }, { @@ -290,9 +297,9 @@ "user_expressions": [] }, "source": [ - "We now have 19 available Argo profiles, each containing `temperature` and `pressure`, compiled into a Pandas DataFrame. BGC Argo is also available through QUEST, so we could add more variables to this list.\n", + "We now have 13 available Argo profiles, each containing `temperature` and `pressure`, compiled into a Pandas DataFrame. BGC Argo is also available through QUEST, so we could add more variables to this list.\n", "\n", - "If the user wishes to add more profiles, parameters, and/or pressure ranges to a pre-existing DataFrame, then they should use `reg_a.download_all(path, keep_existing=True)` to retain previously queried data." + "If the user wishes to add more profiles, parameters, and/or pressure ranges to a pre-existing DataFrame, then they should use `reg_a.datasets['argo'].download(keep_existing=True)` to retain previously downloaded data and have the new data added." ] }, { @@ -316,9 +323,10 @@ }, "outputs": [], "source": [ - "#path_root = '/icepyx/quest-test-data/'\n", - "path_root = '/icepyx/quest-test-data/processed_ATL03_20220419002753_04111506_006_02.h5'\n", - "reader = ipx.Read(path_root)" + "# filename = 'processed_ATL03_20220419002753_04111506_006_02.h5'\n", + "filename = 'processed_ATL03_20220412122813_03121502_006_02.h5'\n", + "\n", + "reader = ipx.Read(path+filename)" ] }, { @@ -594,7 +602,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.10" } }, "nbformat": 4, From 24de1bc0716b557dfd64d0625428c05b1b9af62e Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Tue, 28 Nov 2023 10:56:42 -0500 Subject: [PATCH 111/124] limit example notebook to one IS2 granule --- .../QUEST_argo_data_access.ipynb | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb index f8a66fe7f..f490932b0 100644 --- a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb +++ b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb @@ -49,7 +49,7 @@ "\n", "Just like the ICESat-2 Query object, we begin by defining our Quest object. We provide the following bounding parameters:\n", "* `spatial_extent`: Data is constrained to the given box over the Pacific Ocean.\n", - "* `date_range`: Only grab data from April 12-26, 2022." + "* `date_range`: Only grab data from April 18-19, 2022 (to keep download sizes small for this example)." ] }, { @@ -65,7 +65,7 @@ "spatial_extent = [-154, 30, -143, 37]\n", "\n", "# Start and end dates, in YYYY-MM-DD format\n", - "date_range = ['2022-04-12', '2022-04-14']\n", + "date_range = ['2022-04-18', '2022-04-19']\n", "\n", "# Initialize the QUEST object\n", "reg_a = ipx.Quest(spatial_extent=spatial_extent, date_range=date_range)\n", @@ -297,7 +297,7 @@ "user_expressions": [] }, "source": [ - "We now have 13 available Argo profiles, each containing `temperature` and `pressure`, compiled into a Pandas DataFrame. BGC Argo is also available through QUEST, so we could add more variables to this list.\n", + "We now have two available Argo profiles, each containing `temperature` and `pressure`, compiled into a Pandas DataFrame. BGC Argo is also available through QUEST, so we could add more variables to this list.\n", "\n", "If the user wishes to add more profiles, parameters, and/or pressure ranges to a pre-existing DataFrame, then they should use `reg_a.datasets['argo'].download(keep_existing=True)` to retain previously downloaded data and have the new data added." ] @@ -309,9 +309,9 @@ "user_expressions": [] }, "source": [ - "The download function also provided a series of files containing ICESat-2 ATL03 data. Because these data files are very large, we are only going to focus on one file for this example.\n", + "The download function also provided a file containing ICESat-2 ATL03 data. Recall that because these data files are very large, we focus on only one file for this example.\n", "\n", - "The below workflow uses the icepyx Read module to quickly load ICESat-2 data into the XArray format." + "The below workflow uses the icepyx Read module to quickly load ICESat-2 data into the XArray format. To read in multiple files, see the [icepyx Read tutorial](https://icepyx.readthedocs.io/en/latest/example_notebooks/IS2_data_read-in.html) for how to change your input source." ] }, { @@ -323,10 +323,9 @@ }, "outputs": [], "source": [ - "# filename = 'processed_ATL03_20220419002753_04111506_006_02.h5'\n", - "filename = 'processed_ATL03_20220412122813_03121502_006_02.h5'\n", + "filename = 'processed_ATL03_20220419002753_04111506_006_02.h5'\n", "\n", - "reader = ipx.Read(path+filename)" + "reader = ipx.Read(source=path+filename)" ] }, { From 36cac5792edb55633fbf1f8045d1574be1b0b315 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 4 Dec 2023 00:39:51 -0500 Subject: [PATCH 112/124] fix indentation error and make params protected --- icepyx/quest/quest.py | 2 +- icepyx/tests/test_quest_argo.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index 980512104..fb05ca91c 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -234,4 +234,4 @@ def download_all(self, path="", **kwargs): print(msg) except: dataset_name = type(v).__name__ - print("Error downloading data from {0}".format(dataset_name)) + print("Error downloading data from {0}".format(dataset_name)) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index 92a18e933..16acdd81f 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -92,9 +92,9 @@ def test_presRange_setter_invalid_inputs(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) exp = None - assert reg_a.presRange == exp + assert reg_a._presRange == exp - reg_a.presRange = ( + reg_a._presRange = ( "0.5, sam" # it looks like the API will take a string with a space ) From 74c3a6030fa95fd7a3ae1158e0aa5989be1d1cd7 Mon Sep 17 00:00:00 2001 From: Romina Date: Mon, 4 Dec 2023 01:24:19 -0500 Subject: [PATCH 113/124] skips when error downloading argo profile, save df to csv --- icepyx/quest/dataset_scripts/argo.py | 13 +++++++++---- icepyx/quest/quest.py | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index dfe0f692f..222048dea 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -404,7 +404,7 @@ def _parse_into_df(self, profile_data) -> pd.DataFrame: return profileDf - def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFrame: + def download(self, params=None, presRange=None, keep_existing=True, savename='') -> pd.DataFrame: """ Downloads the requested data for a list of profile IDs (stored under .prof_ids) and returns it in a DataFrame. @@ -471,9 +471,12 @@ def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFr merged_df = pd.DataFrame(columns=["profile_id"]) for i in self.prof_ids: print("processing profile", i) - profile_data = self._download_profile(i) - profile_df = self._parse_into_df(profile_data[0]) - merged_df = pd.concat([merged_df, profile_df], sort=False) + try: + profile_data = self._download_profile(i) + profile_df = self._parse_into_df(profile_data[0]) + merged_df = pd.concat([merged_df, profile_df], sort=False) + except: + print('\tError processing profile {0}. Skipping.'.format(i)) # now that we have a df from this round of downloads, we can add it to any existing dataframe # note that if a given column has previously been added, update needs to be used to replace nans (merge will not replace the nan values) @@ -484,4 +487,6 @@ def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFr self.argodata.reset_index(inplace=True, drop=True) + if savename: + self.argodata.to_csv(savename + '_argo.csv') return self.argodata diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index fb05ca91c..ee401792e 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -228,9 +228,9 @@ def download_all(self, path="", **kwargs): else: print(k) try: - msg = v.download(kwargs[k]) + msg = v.download(kwargs[k], savename=path) except KeyError: - msg = v.download() + msg = v.download(savename=path) print(msg) except: dataset_name = type(v).__name__ From 79ecc27e81fbd87bd1710da6a093b88e48b4a8e6 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Mon, 4 Dec 2023 17:56:58 -0500 Subject: [PATCH 114/124] use xarray to drop info before converting to df --- .../QUEST_argo_data_access.ipynb | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb index f490932b0..40094d4ee 100644 --- a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb +++ b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb @@ -325,7 +325,7 @@ "source": [ "filename = 'processed_ATL03_20220419002753_04111506_006_02.h5'\n", "\n", - "reader = ipx.Read(source=path+filename)" + "reader = ipx.Read(data_source=path+filename)" ] }, { @@ -361,19 +361,31 @@ "user_expressions": [] }, "source": [ - "To make the data more easily plottable, let's convert the data into a Pandas DataFrame. Note that this method is memory-intensive for ATL03 data, so users are suggested to look at small spatial domains to prevent the notebook from crashing." + "To make the data more easily plottable, let's convert the data into a Pandas DataFrame. Note that this method is memory-intensive for ATL03 data, so users are suggested to look at small spatial domains to prevent the notebook from crashing. Here, since we only have data from one granule and ground track, we have sped up the conversion to a dataframe by first removing extra xarray dimensions we don't need for our plots. Several of the other steps completed below have analogous operations in xarray that would further reduce memory requirements and computation times." ] }, { "cell_type": "code", "execution_count": null, - "id": "bc086db7-f5a1-4ba7-ba90-5b19afaf6808", - "metadata": { - "tags": [] - }, + "id": "50d23a8e", + "metadata": {}, + "outputs": [], + "source": [ + "is2_pd =(ds.squeeze()\n", + " .reset_coords()\n", + " .drop_vars([\"source_file\",\"data_start_utc\",\"data_end_utc\",\"gran_idx\"])\n", + " .to_dataframe()\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01bb5a12", + "metadata": {}, "outputs": [], "source": [ - "is2_pd = ds.to_dataframe()" + "is2_pd" ] }, { @@ -385,9 +397,9 @@ }, "outputs": [], "source": [ - "# Rearrange the data to only include \"ocean\" photons\n", - "is2_pd = is2_pd.reset_index(level=[0,1,2])\n", - "is2_pd_ocean = is2_pd[is2_pd.index==1]\n", + "# Create a new dataframe with only \"ocean\" photons, as indicated by the \"ds_surf_type\" flag\n", + "is2_pd = is2_pd.reset_index(level=[0,1])\n", + "is2_pd_ocean = is2_pd[is2_pd.ds_surf_type==1].drop(columns=\"photon_idx\")\n", "is2_pd_ocean" ] }, @@ -446,7 +458,7 @@ "outputs": [], "source": [ "# Drop time variables that would cause errors in explore() function\n", - "is2_gdf = is2_gdf.drop(['data_start_utc','data_end_utc','delta_time','atlas_sdp_gps_epoch'], axis=1)" + "is2_gdf = is2_gdf.drop(['delta_time','atlas_sdp_gps_epoch'], axis=1)" ] }, { From 089f71e28a30ef089345da573c2753aa59ba2e1a Mon Sep 17 00:00:00 2001 From: Romina Date: Wed, 6 Dec 2023 11:20:14 -0500 Subject: [PATCH 115/124] implement save function in argo --- icepyx/quest/dataset_scripts/argo.py | 28 ++++++++++++++++++++++--- icepyx/quest/dataset_scripts/dataset.py | 6 ++++++ icepyx/quest/quest.py | 10 +++++++++ icepyx/tests/test_quest_argo.py | 19 ++++++++++++++--- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 222048dea..df67c4b1b 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -1,3 +1,5 @@ +import os.path + import numpy as np import pandas as pd import requests @@ -404,7 +406,7 @@ def _parse_into_df(self, profile_data) -> pd.DataFrame: return profileDf - def download(self, params=None, presRange=None, keep_existing=True, savename='') -> pd.DataFrame: + def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFrame: """ Downloads the requested data for a list of profile IDs (stored under .prof_ids) and returns it in a DataFrame. @@ -487,6 +489,26 @@ def download(self, params=None, presRange=None, keep_existing=True, savename='') self.argodata.reset_index(inplace=True, drop=True) - if savename: - self.argodata.to_csv(savename + '_argo.csv') return self.argodata + + def save(self, filepath): + """ + Saves the argo dataframe to a csv at the specified location + + Parameters + ---------- + filepath: string containing complete filepath and name of file + extension will be removed and replaced with csv. Also appends + '_argo.csv' to filename + e.g. /path/to/file/my_data(.csv) + """ + + + # create the directory if it doesn't exist + path, file = os.path.split(filepath) + if not os.path.exists(path): + os.mkdir(path) + + # remove file extension + base, ext = os.path.splitext(filepath) + self.argodata.to_csv(base + '_argo.csv') diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index 9c017e1ca..8990686f9 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -69,6 +69,12 @@ def download(self): """ raise NotImplementedError + def save(self, filepath): + """ + Save the downloaded data to a directory on your local machine. + """ + raise NotImplementedError + # ---------------------------------------------------------------------- # Working with Data diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index ee401792e..b919552fa 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -235,3 +235,13 @@ def download_all(self, path="", **kwargs): except: dataset_name = type(v).__name__ print("Error downloading data from {0}".format(dataset_name)) + + + def save_all(self, path): + + for k, v in self.datasets.items(): + if isinstance(v, Query): + print("ICESat-2 granules are saved during download") + else: + print("Saving " + k) + v.save(path) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index 16acdd81f..3497da672 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -1,3 +1,5 @@ +import os + import pytest import re @@ -92,9 +94,9 @@ def test_presRange_setter_invalid_inputs(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) exp = None - assert reg_a._presRange == exp + assert reg_a.presRange == exp - reg_a._presRange = ( + reg_a.presRange = ( "0.5, sam" # it looks like the API will take a string with a space ) @@ -158,6 +160,16 @@ def test_download_parse_into_df(argo_quest_instance): # approach for additional testing of df functions: create json files with profiles and store them in test suite # then use those for the comparison (e.g. number of rows in df and json match) +def test_save_df_to_csv(argo_quest_instance): + reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) + reg_a.download() # note: pressure is returned by default + + + path = os.getcwd() + "test_file" + reg_a.save(path) + + assert os.path.exists(path + "_argo.csv") + os.remove(path + "_argo.csv") def test_merge_df(argo_quest_instance): reg_a = argo_quest_instance([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) @@ -175,10 +187,11 @@ def test_merge_df(argo_quest_instance): assert "down_irradiance412_argoqc" in df.columns + + # --------------------------------------------------- # Test kwargs to replace params and presRange in search and download - def test_replace_param_search(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) From 40a4ca34ec0008c84ca5f3fe96b151b5745c177e Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 6 Dec 2023 11:41:03 -0500 Subject: [PATCH 116/124] run black formatter on all files --- doc/sphinxext/announce.py | 2 - icepyx/core/APIformatting.py | 1 - icepyx/core/auth.py | 71 +++++++++++++----------- icepyx/core/exceptions.py | 3 +- icepyx/core/icesat2data.py | 5 +- icepyx/core/query.py | 7 +-- icepyx/core/spatial.py | 3 - icepyx/core/temporal.py | 8 --- icepyx/core/validate_inputs.py | 10 ++-- icepyx/core/variables.py | 48 ++++++++-------- icepyx/core/visualization.py | 4 -- icepyx/quest/dataset_scripts/__init__.py | 2 +- icepyx/quest/dataset_scripts/argo.py | 6 +- icepyx/quest/dataset_scripts/dataset.py | 1 - icepyx/quest/quest.py | 3 - icepyx/tests/conftest.py | 1 + icepyx/tests/test_APIformatting.py | 1 + icepyx/tests/test_Earthdata.py | 2 +- icepyx/tests/test_auth.py | 15 +++-- icepyx/tests/test_query.py | 1 + icepyx/tests/test_quest.py | 2 + icepyx/tests/test_quest_argo.py | 7 ++- icepyx/tests/test_read.py | 2 - icepyx/tests/test_spatial.py | 2 - icepyx/tests/test_temporal.py | 1 + icepyx/tests/test_visualization.py | 1 - 26 files changed, 102 insertions(+), 107 deletions(-) diff --git a/doc/sphinxext/announce.py b/doc/sphinxext/announce.py index 21bf7a69e..6a4264349 100644 --- a/doc/sphinxext/announce.py +++ b/doc/sphinxext/announce.py @@ -76,7 +76,6 @@ def get_authors(revision_range): # "Co-authored by" commits, which come from backports by the bot, # and one for regular commits. if ".mailmap" in os.listdir(this_repo.git.working_dir): - xpr = re.compile(r"Co-authored-by: (?P[^<]+) ") gitcur = list(os.popen("git shortlog -s " + revision_range).readlines()) @@ -94,7 +93,6 @@ def get_authors(revision_range): pre = set(pre) else: - xpr = re.compile(r"Co-authored-by: (?P[^<]+) ") cur = set( xpr.findall( diff --git a/icepyx/core/APIformatting.py b/icepyx/core/APIformatting.py index 55d49f84c..b5d31bdfa 100644 --- a/icepyx/core/APIformatting.py +++ b/icepyx/core/APIformatting.py @@ -205,7 +205,6 @@ class Parameters: """ def __init__(self, partype, values=None, reqtype=None): - assert partype in [ "CMR", "required", diff --git a/icepyx/core/auth.py b/icepyx/core/auth.py index 7c36126f9..cf771f420 100644 --- a/icepyx/core/auth.py +++ b/icepyx/core/auth.py @@ -4,14 +4,16 @@ import earthaccess + class AuthenticationError(Exception): - ''' + """ Raised when an error is encountered while authenticating Earthdata credentials - ''' + """ + pass -class EarthdataAuthMixin(): +class EarthdataAuthMixin: """ This mixin class generates the needed authentication sessions and tokens, including for NASA Earthdata cloud access. Authentication is completed using the [earthaccess library](https://nsidc.github.io/earthaccess/). @@ -21,26 +23,27 @@ class EarthdataAuthMixin(): 3. Storing credentials in a .netrc file (not recommended for security reasons) More details on using these methods is available in the [earthaccess documentation](https://nsidc.github.io/earthaccess/tutorials/restricted-datasets/#auth). - This class can be inherited by any other class that requires authentication. For - example, the `Query` class inherits this one, and so a Query object has the + This class can be inherited by any other class that requires authentication. For + example, the `Query` class inherits this one, and so a Query object has the `.session` property. The method `earthdata_login()` is included for backwards compatibility. - + The class can be created without any initialization parameters, and the properties will - be populated when they are called. It can alternately be initialized with an - earthaccess.auth.Auth object, which will then be used to create a session or + be populated when they are called. It can alternately be initialized with an + earthaccess.auth.Auth object, which will then be used to create a session or s3login_credentials as they are called. - + Parameters ---------- auth : earthaccess.auth.Auth, default None Optional parameter to initialize an object with existing credentials. - + Examples -------- >>> a = EarthdataAuthMixin() >>> a.session # doctest: +SKIP >>> a.s3login_credentials # doctest: +SKIP """ + def __init__(self, auth=None): self._auth = copy.deepcopy(auth) # initializatin of session and s3 creds is not allowed because those are generated @@ -58,25 +61,27 @@ def __str__(self): @property def auth(self): - ''' - Authentication object returned from earthaccess.login() which stores user authentication. - ''' + """ + Authentication object returned from earthaccess.login() which stores user authentication. + """ # Only login the first time .auth is accessed if self._auth is None: auth = earthaccess.login() # check for a valid auth response if auth.authenticated is False: - raise AuthenticationError('Earthdata authentication failed. Check output for error message') + raise AuthenticationError( + "Earthdata authentication failed. Check output for error message" + ) else: self._auth = auth - + return self._auth @property def session(self): - ''' + """ Earthaccess session object for connecting to Earthdata resources. - ''' + """ # Only generate a session the first time .session is accessed if self._session is None: self._session = self.auth.get_session() @@ -84,24 +89,26 @@ def session(self): @property def s3login_credentials(self): - ''' + """ A dictionary which stores login credentials for AWS s3 access. This property is accessed if using AWS cloud data. - + Because s3 tokens are only good for one hour, this function will automatically check if an hour has elapsed since the last token use and generate a new token if necessary. - ''' - + """ + def set_s3_creds(): - ''' Store s3login creds from `auth`and reset the last updated timestamp''' + """Store s3login creds from `auth`and reset the last updated timestamp""" self._s3login_credentials = self.auth.get_s3_credentials(daac="NSIDC") self._s3_initial_ts = datetime.datetime.now() - + # Only generate s3login_credentials the first time credentials are accessed, or if an hour - # has passed since the last login + # has passed since the last login if self._s3login_credentials is None: set_s3_creds() - elif (datetime.datetime.now() - self._s3_initial_ts) >= datetime.timedelta(hours=1): + elif (datetime.datetime.now() - self._s3_initial_ts) >= datetime.timedelta( + hours=1 + ): set_s3_creds() return self._s3login_credentials @@ -109,7 +116,7 @@ def earthdata_login(self, uid=None, email=None, s3token=None, **kwargs) -> None: """ Authenticate with NASA Earthdata to enable data ordering and download. Credential storage details are described in the EathdataAuthMixin class section. - + **Note:** This method is maintained for backward compatibility. It is no longer required to explicitly run `.earthdata_login()`. Authentication will be performed by the module as needed when `.session` or `.s3login_credentials` are accessed. Parameters @@ -134,12 +141,14 @@ def earthdata_login(self, uid=None, email=None, s3token=None, **kwargs) -> None: """ warnings.warn( - "It is no longer required to explicitly run the `.earthdata_login()` method. Authentication will be performed by the module as needed.", - DeprecationWarning, stacklevel=2 - ) - + "It is no longer required to explicitly run the `.earthdata_login()` method. Authentication will be performed by the module as needed.", + DeprecationWarning, + stacklevel=2, + ) + if uid != None or email != None or s3token != None: warnings.warn( "The user id (uid) and/or email keyword arguments are no longer required.", - DeprecationWarning, stacklevel=2 + DeprecationWarning, + stacklevel=2, ) diff --git a/icepyx/core/exceptions.py b/icepyx/core/exceptions.py index a36a1b645..d20bbfe61 100644 --- a/icepyx/core/exceptions.py +++ b/icepyx/core/exceptions.py @@ -2,6 +2,7 @@ class DeprecationError(Exception): """ Class raised for use of functionality that is no longer supported by icepyx. """ + pass @@ -27,5 +28,3 @@ def __init__( def __str__(self): return f"{self.msgtxt}: {self.errmsg}" - - diff --git a/icepyx/core/icesat2data.py b/icepyx/core/icesat2data.py index cebce4160..aa35fd433 100644 --- a/icepyx/core/icesat2data.py +++ b/icepyx/core/icesat2data.py @@ -2,8 +2,9 @@ class Icesat2Data: - def __init__(self,): - + def __init__( + self, + ): warnings.filterwarnings("always") warnings.warn( "DEPRECATED. Please use icepyx.Query to create a download data object (all other functionality is the same)", diff --git a/icepyx/core/query.py b/icepyx/core/query.py index 4ffe4c241..d857bbb3d 100644 --- a/icepyx/core/query.py +++ b/icepyx/core/query.py @@ -351,9 +351,9 @@ class Query(GenQuery, EarthdataAuthMixin): files : string, default None A placeholder for future development. Not used for any purposes yet. auth : earthaccess.auth.Auth, default None - An earthaccess authentication object. Available as an argument so an existing - earthaccess.auth.Auth object can be used for authentication. If not given, a new auth - object will be created whenever authentication is needed. + An earthaccess authentication object. Available as an argument so an existing + earthaccess.auth.Auth object can be used for authentication. If not given, a new auth + object will be created whenever authentication is needed. Returns ------- @@ -411,7 +411,6 @@ def __init__( auth=None, **kwargs, ): - # Check necessary combination of input has been specified if ( (product is None or spatial_extent is None) diff --git a/icepyx/core/spatial.py b/icepyx/core/spatial.py index 7702acdf2..c34e928ed 100644 --- a/icepyx/core/spatial.py +++ b/icepyx/core/spatial.py @@ -80,7 +80,6 @@ def geodataframe(extent_type, spatial_extent, file=False, xdateline=None): # DevGoal: the crs setting and management needs to be improved elif extent_type == "polygon" and file == False: - # if spatial_extent is already a Polygon if isinstance(spatial_extent, Polygon): spatial_extent_geom = spatial_extent @@ -248,7 +247,6 @@ def validate_polygon_pairs(spatial_extent): if (spatial_extent[0][0] != spatial_extent[-1][0]) or ( spatial_extent[0][1] != spatial_extent[-1][1] ): - # Throw a warning warnings.warn( "WARNING: Polygon's first and last point's coordinates differ," @@ -436,7 +434,6 @@ def __init__(self, spatial_extent, **kwarg): # Check if spatial_extent is a list of coordinates (bounding box or polygon) if isinstance(spatial_extent, (list, np.ndarray)): - # bounding box if len(spatial_extent) == 4 and all( isinstance(i, scalar_types) for i in spatial_extent diff --git a/icepyx/core/temporal.py b/icepyx/core/temporal.py index c7e2dda1c..67f59882a 100644 --- a/icepyx/core/temporal.py +++ b/icepyx/core/temporal.py @@ -51,7 +51,6 @@ def convert_string_to_date(date): def check_valid_date_range(start, end): - """ Helper function for checking if a date range is valid. @@ -89,7 +88,6 @@ def check_valid_date_range(start, end): def validate_times(start_time, end_time): - """ Validates the start and end times passed into __init__ and returns them as datetime.time objects. @@ -145,7 +143,6 @@ def validate_times(start_time, end_time): def validate_date_range_datestr(date_range, start_time=None, end_time=None): - """ Validates a date range provided in the form of a list of strings. @@ -190,7 +187,6 @@ def validate_date_range_datestr(date_range, start_time=None, end_time=None): def validate_date_range_datetime(date_range, start_time=None, end_time=None): - """ Validates a date range provided in the form of a list of datetimes. @@ -230,7 +226,6 @@ def validate_date_range_datetime(date_range, start_time=None, end_time=None): def validate_date_range_date(date_range, start_time=None, end_time=None): - """ Validates a date range provided in the form of a list of datetime.date objects. @@ -268,7 +263,6 @@ def validate_date_range_date(date_range, start_time=None, end_time=None): def validate_date_range_dict(date_range, start_time=None, end_time=None): - """ Validates a date range provided in the form of a dict with the following keys: @@ -330,7 +324,6 @@ def validate_date_range_dict(date_range, start_time=None, end_time=None): # if is string date elif isinstance(_start_date, str): - _start_date = convert_string_to_date(_start_date) _start_date = dt.datetime.combine(_start_date, start_time) @@ -411,7 +404,6 @@ def __init__(self, date_range, start_time=None, end_time=None): """ if len(date_range) == 2: - # date range is provided as dict of strings, dates, or datetimes if isinstance(date_range, dict): self._start, self._end = validate_date_range_dict( diff --git a/icepyx/core/validate_inputs.py b/icepyx/core/validate_inputs.py index d74768eea..a69f045fb 100644 --- a/icepyx/core/validate_inputs.py +++ b/icepyx/core/validate_inputs.py @@ -105,15 +105,17 @@ def tracks(track): return track_list + def check_s3bucket(path): """ Check if the given path is an s3 path. Raise a warning if the data being referenced is not in the NSIDC bucket """ - split_path = path.split('/') - if split_path[0] == 's3:' and split_path[2] != 'nsidc-cumulus-prod-protected': + split_path = path.split("/") + if split_path[0] == "s3:" and split_path[2] != "nsidc-cumulus-prod-protected": warnings.warn( - 's3 data being read from outside the NSIDC data bucket. Icepyx can ' - 'read this data, but available data lists may not be accurate.', stacklevel=2 + "s3 data being read from outside the NSIDC data bucket. Icepyx can " + "read this data, but available data lists may not be accurate.", + stacklevel=2, ) return path diff --git a/icepyx/core/variables.py b/icepyx/core/variables.py index 4c52003df..4dd5444fe 100644 --- a/icepyx/core/variables.py +++ b/icepyx/core/variables.py @@ -29,7 +29,7 @@ class Variables(EarthdataAuthMixin): contained in ICESat-2 products. Parameters - ---------- + ---------- vartype : string This argument is deprecated. The vartype will be inferred from data_source. One of ['order', 'file'] to indicate the source of the input variables. @@ -49,9 +49,9 @@ class Variables(EarthdataAuthMixin): wanted : dictionary, default None As avail, but for the desired list of variables auth : earthaccess.auth.Auth, default None - An earthaccess authentication object. Available as an argument so an existing - earthaccess.auth.Auth object can be used for authentication. If not given, a new auth - object will be created whenever authentication is needed. + An earthaccess authentication object. Available as an argument so an existing + earthaccess.auth.Auth object can be used for authentication. If not given, a new auth + object will be created whenever authentication is needed. """ def __init__( @@ -65,28 +65,28 @@ def __init__( auth=None, ): # Deprecation error - if vartype in ['order', 'file']: + if vartype in ["order", "file"]: raise DeprecationError( - 'It is no longer required to specify the variable type `vartype`. Instead please ', - 'provide either the path to a local file (arg: `path`) or the product you would ', - 'like variables for (arg: `product`).' + "It is no longer required to specify the variable type `vartype`. Instead please ", + "provide either the path to a local file (arg: `path`) or the product you would ", + "like variables for (arg: `product`).", ) - + if path and product: raise TypeError( - 'Please provide either a path or a product. If a path is provided ', - 'variables will be read from the file. If a product is provided all available ', - 'variables for that product will be returned.' + "Please provide either a path or a product. If a path is provided ", + "variables will be read from the file. If a product is provided all available ", + "variables for that product will be returned.", ) # initialize authentication properties EarthdataAuthMixin.__init__(self, auth=auth) - + # Set the product and version from either the input args or the file if path: self._path = val.check_s3bucket(path) # Set up auth - if self._path.startswith('s3'): + if self._path.startswith("s3"): auth = self.auth else: auth = None @@ -98,15 +98,19 @@ def __init__( self._product = is2ref._validate_product(product) # Check for valid version string # If version is not specified by the user assume the most recent version - self._version = val.prod_version(is2ref.latest_version(self._product), version) + self._version = val.prod_version( + is2ref.latest_version(self._product), version + ) else: - raise TypeError('Either a path or a product need to be given as input arguments.') - + raise TypeError( + "Either a path or a product need to be given as input arguments." + ) + self._avail = avail self.wanted = wanted # DevGoal: put some more/robust checks here to assess validity of inputs - + @property def path(self): if self._path: @@ -114,15 +118,14 @@ def path(self): else: path = None return path - + @property def product(self): return self._product - + @property def version(self): return self._version - def avail(self, options=False, internal=False): """ @@ -143,7 +146,7 @@ def avail(self, options=False, internal=False): """ if not hasattr(self, "_avail") or self._avail == None: - if not hasattr(self, 'path') or self.path.startswith('s3'): + if not hasattr(self, "path") or self.path.startswith("s3"): self._avail = is2ref._get_custom_options( self.session, self.product, self.version )["variables"] @@ -628,7 +631,6 @@ def remove(self, all=False, var_list=None, beam_list=None, keyword_list=None): for bkw in beam_list: if bkw in vpath_kws: for kw in keyword_list: - if kw in vpath_kws: self.wanted[vkey].remove(vpath) except TypeError: diff --git a/icepyx/core/visualization.py b/icepyx/core/visualization.py index 32c81e3e7..001ae178e 100644 --- a/icepyx/core/visualization.py +++ b/icepyx/core/visualization.py @@ -142,7 +142,6 @@ def __init__( cycles=None, tracks=None, ): - if query_obj: pass else: @@ -241,7 +240,6 @@ def query_icesat2_filelist(self) -> tuple: is2_file_list = [] for bbox_i in bbox_list: - try: region = ipx.Query( self.product, @@ -364,7 +362,6 @@ def request_OA_data(self, paras) -> da.array: # get data we need (with the correct date) try: - df_series = df.query(expr="date == @Date").iloc[0] beam_data = df_series.beams @@ -483,7 +480,6 @@ def viz_elevation(self) -> (hv.DynamicMap, hv.Layout): return (None,) * 2 else: - cols = ( ["lat", "lon", "elevation", "canopy", "rgt", "cycle"] if self.product == "ATL08" diff --git a/icepyx/quest/dataset_scripts/__init__.py b/icepyx/quest/dataset_scripts/__init__.py index c7b28ee49..7834127ff 100644 --- a/icepyx/quest/dataset_scripts/__init__.py +++ b/icepyx/quest/dataset_scripts/__init__.py @@ -1 +1 @@ -from .dataset import * \ No newline at end of file +from .dataset import * diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index df67c4b1b..00a57a9f3 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -46,7 +46,6 @@ def __init__(self, aoi, toi, params=["temperature"], presRange=None): self._apikey = "92259861231b55d32a9c0e4e3a93f4834fc0b6fa" def __str__(self): - if self.presRange is None: prange = "All" else: @@ -478,7 +477,7 @@ def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFr profile_df = self._parse_into_df(profile_data[0]) merged_df = pd.concat([merged_df, profile_df], sort=False) except: - print('\tError processing profile {0}. Skipping.'.format(i)) + print("\tError processing profile {0}. Skipping.".format(i)) # now that we have a df from this round of downloads, we can add it to any existing dataframe # note that if a given column has previously been added, update needs to be used to replace nans (merge will not replace the nan values) @@ -503,7 +502,6 @@ def save(self, filepath): e.g. /path/to/file/my_data(.csv) """ - # create the directory if it doesn't exist path, file = os.path.split(filepath) if not os.path.exists(path): @@ -511,4 +509,4 @@ def save(self, filepath): # remove file extension base, ext = os.path.splitext(filepath) - self.argodata.to_csv(base + '_argo.csv') + self.argodata.to_csv(base + "_argo.csv") diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index 8990686f9..193fab22e 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -11,7 +11,6 @@ class DataSet: All sub-classes must support the following methods for use via the QUEST class. """ - def __init__(self, spatial_extent, date_range, start_time=None, end_time=None): """ Complete any dataset specific initializations (i.e. beyond space and time) required here. diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index b919552fa..3b8620681 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -217,7 +217,6 @@ def download_all(self, path="", **kwargs): print() try: - if isinstance(v, Query): print("---ICESat-2---") try: @@ -236,9 +235,7 @@ def download_all(self, path="", **kwargs): dataset_name = type(v).__name__ print("Error downloading data from {0}".format(dataset_name)) - def save_all(self, path): - for k, v in self.datasets.items(): if isinstance(v, Query): print("ICESat-2 granules are saved during download") diff --git a/icepyx/tests/conftest.py b/icepyx/tests/conftest.py index fca31847a..9ce8e4081 100644 --- a/icepyx/tests/conftest.py +++ b/icepyx/tests/conftest.py @@ -2,6 +2,7 @@ import pytest from unittest import mock + # PURPOSE: mock environmental variables @pytest.fixture(scope="session", autouse=True) def mock_settings_env_vars(): diff --git a/icepyx/tests/test_APIformatting.py b/icepyx/tests/test_APIformatting.py index 83e88a131..213c1cf8a 100644 --- a/icepyx/tests/test_APIformatting.py +++ b/icepyx/tests/test_APIformatting.py @@ -11,6 +11,7 @@ # CMR temporal and spatial formats --> what's the best way to compare formatted text? character by character comparison of strings? + ########## _fmt_temporal ########## def test_time_fmt(): obs = apifmt._fmt_temporal( diff --git a/icepyx/tests/test_Earthdata.py b/icepyx/tests/test_Earthdata.py index 8ad883e6a..60b92f621 100644 --- a/icepyx/tests/test_Earthdata.py +++ b/icepyx/tests/test_Earthdata.py @@ -8,6 +8,7 @@ import shutil import warnings + # PURPOSE: test different authentication methods @pytest.fixture(scope="module", autouse=True) def setup_earthdata(): @@ -65,7 +66,6 @@ def earthdata_login(uid=None, pwd=None, email=None, s3token=False) -> bool: url = "urs.earthdata.nasa.gov" mock_uid, _, mock_pwd = netrc.netrc(netrc).authenticators(url) except: - mock_uid = os.environ.get("EARTHDATA_USERNAME") mock_pwd = os.environ.get("EARTHDATA_PASSWORD") diff --git a/icepyx/tests/test_auth.py b/icepyx/tests/test_auth.py index 6ac77c864..8507b1e40 100644 --- a/icepyx/tests/test_auth.py +++ b/icepyx/tests/test_auth.py @@ -8,30 +8,35 @@ @pytest.fixture() def auth_instance(): - ''' + """ An EarthdatAuthMixin object for each of the tests. Default scope is function level, so a new instance should be created for each of the tests. - ''' + """ return EarthdataAuthMixin() + # Test that .session creates a session def test_get_session(auth_instance): assert isinstance(auth_instance.session, requests.sessions.Session) + # Test that .s3login_credentials creates a dict with the correct keys def test_get_s3login_credentials(auth_instance): assert isinstance(auth_instance.s3login_credentials, dict) - expected_keys = set(['accessKeyId', 'secretAccessKey', 'sessionToken', - 'expiration']) + expected_keys = set( + ["accessKeyId", "secretAccessKey", "sessionToken", "expiration"] + ) assert set(auth_instance.s3login_credentials.keys()) == expected_keys + # Test that earthdata_login generates an auth object def test_login_function(auth_instance): auth_instance.earthdata_login() assert isinstance(auth_instance.auth, earthaccess.auth.Auth) assert auth_instance.auth.authenticated + # Test that earthdata_login raises a warning if email is provided def test_depreciation_warning(auth_instance): with pytest.warns(DeprecationWarning): - auth_instance.earthdata_login(email='me@gmail.com') + auth_instance.earthdata_login(email="me@gmail.com") diff --git a/icepyx/tests/test_query.py b/icepyx/tests/test_query.py index 7738c424a..15eebfcbd 100644 --- a/icepyx/tests/test_query.py +++ b/icepyx/tests/test_query.py @@ -9,6 +9,7 @@ # seem to be adequately covered in docstrings; # may want to focus on testing specific queries + # ------------------------------------ # icepyx-specific tests # ------------------------------------ diff --git a/icepyx/tests/test_quest.py b/icepyx/tests/test_quest.py index 8746718a8..0ba7325a6 100644 --- a/icepyx/tests/test_quest.py +++ b/icepyx/tests/test_quest.py @@ -15,6 +15,7 @@ def quest_instance(scope="module", autouse=True): ########## PER-DATASET ADDITION TESTS ########## + # Paramaterize these add_dataset tests once more datasets are added def test_add_is2(quest_instance): # Add ATL06 as a test to QUEST @@ -59,6 +60,7 @@ def test_add_multiple_datasets(quest_instance): ########## ALL DATASET METHODS TESTS ########## + # each of the query functions should be tested in their respective modules def test_search_all(quest_instance): quest_instance.add_argo(params=["down_irradiance412", "temperature"]) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index 3497da672..a6940fe7b 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -5,6 +5,7 @@ from icepyx.quest.quest import Quest + # create an Argo instance via quest (Argo is a submodule) @pytest.fixture(scope="function") def argo_quest_instance(): @@ -160,17 +161,18 @@ def test_download_parse_into_df(argo_quest_instance): # approach for additional testing of df functions: create json files with profiles and store them in test suite # then use those for the comparison (e.g. number of rows in df and json match) + def test_save_df_to_csv(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) reg_a.download() # note: pressure is returned by default - path = os.getcwd() + "test_file" reg_a.save(path) assert os.path.exists(path + "_argo.csv") os.remove(path + "_argo.csv") + def test_merge_df(argo_quest_instance): reg_a = argo_quest_instance([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) param_list = ["salinity", "temperature", "down_irradiance412"] @@ -187,11 +189,10 @@ def test_merge_df(argo_quest_instance): assert "down_irradiance412_argoqc" in df.columns - - # --------------------------------------------------- # Test kwargs to replace params and presRange in search and download + def test_replace_param_search(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) diff --git a/icepyx/tests/test_read.py b/icepyx/tests/test_read.py index 018435968..d6727607e 100644 --- a/icepyx/tests/test_read.py +++ b/icepyx/tests/test_read.py @@ -21,7 +21,6 @@ def test_check_datasource_type(): ], ) def test_check_datasource(filepath, expect): - source_type = read._check_datasource(filepath) assert source_type == expect @@ -90,7 +89,6 @@ def test_validate_source_str_not_a_dir_or_file(): ], ) def test_check_run_fast_scandir(dir, fn_glob, expect): - (subfolders, files) = read._run_fast_scandir(dir, fn_glob) assert (sorted(subfolders), sorted(files)) == expect diff --git a/icepyx/tests/test_spatial.py b/icepyx/tests/test_spatial.py index 2666d857d..4d6369d9e 100644 --- a/icepyx/tests/test_spatial.py +++ b/icepyx/tests/test_spatial.py @@ -351,7 +351,6 @@ def test_poly_list_auto_close(): def test_poly_file_simple_one_poly(): - poly_from_file = spat.Spatial( str( Path( @@ -391,7 +390,6 @@ def test_bad_poly_inputfile_type_throws_error(): def test_gdf_from_one_bbox(): - obs = spat.geodataframe("bounding_box", [-55, 68, -48, 71]) geom = [Polygon(list(zip([-55, -55, -48, -48, -55], [68, 71, 71, 68, 68])))] exp = gpd.GeoDataFrame(geometry=geom) diff --git a/icepyx/tests/test_temporal.py b/icepyx/tests/test_temporal.py index 83926946e..c93b30a38 100644 --- a/icepyx/tests/test_temporal.py +++ b/icepyx/tests/test_temporal.py @@ -235,6 +235,7 @@ def test_range_str_yyyydoy_dict_time_start_end(): # Date Range Errors + # (The following inputs are bad, testing to ensure the temporal class handles this elegantly) def test_bad_start_time_type(): with pytest.raises(AssertionError): diff --git a/icepyx/tests/test_visualization.py b/icepyx/tests/test_visualization.py index 0a1f2fa43..dfd41116f 100644 --- a/icepyx/tests/test_visualization.py +++ b/icepyx/tests/test_visualization.py @@ -62,7 +62,6 @@ def test_files_in_latest_cycles(n, exp): ], ) def test_gran_paras(filename, expect): - para_list = vis.gran_paras(filename) assert para_list == expect From 2fdcdd26bc112778bc442b473ec291862a32b4bc Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 6 Dec 2023 11:41:53 -0500 Subject: [PATCH 117/124] Revert "run black formatter on all files" This reverts commit 40a4ca34ec0008c84ca5f3fe96b151b5745c177e. --- doc/sphinxext/announce.py | 2 + icepyx/core/APIformatting.py | 1 + icepyx/core/auth.py | 71 +++++++++++------------- icepyx/core/exceptions.py | 3 +- icepyx/core/icesat2data.py | 5 +- icepyx/core/query.py | 7 ++- icepyx/core/spatial.py | 3 + icepyx/core/temporal.py | 8 +++ icepyx/core/validate_inputs.py | 10 ++-- icepyx/core/variables.py | 48 ++++++++-------- icepyx/core/visualization.py | 4 ++ icepyx/quest/dataset_scripts/__init__.py | 2 +- icepyx/quest/dataset_scripts/argo.py | 6 +- icepyx/quest/dataset_scripts/dataset.py | 1 + icepyx/quest/quest.py | 3 + icepyx/tests/conftest.py | 1 - icepyx/tests/test_APIformatting.py | 1 - icepyx/tests/test_Earthdata.py | 2 +- icepyx/tests/test_auth.py | 15 ++--- icepyx/tests/test_query.py | 1 - icepyx/tests/test_quest.py | 2 - icepyx/tests/test_quest_argo.py | 7 +-- icepyx/tests/test_read.py | 2 + icepyx/tests/test_spatial.py | 2 + icepyx/tests/test_temporal.py | 1 - icepyx/tests/test_visualization.py | 1 + 26 files changed, 107 insertions(+), 102 deletions(-) diff --git a/doc/sphinxext/announce.py b/doc/sphinxext/announce.py index 6a4264349..21bf7a69e 100644 --- a/doc/sphinxext/announce.py +++ b/doc/sphinxext/announce.py @@ -76,6 +76,7 @@ def get_authors(revision_range): # "Co-authored by" commits, which come from backports by the bot, # and one for regular commits. if ".mailmap" in os.listdir(this_repo.git.working_dir): + xpr = re.compile(r"Co-authored-by: (?P[^<]+) ") gitcur = list(os.popen("git shortlog -s " + revision_range).readlines()) @@ -93,6 +94,7 @@ def get_authors(revision_range): pre = set(pre) else: + xpr = re.compile(r"Co-authored-by: (?P[^<]+) ") cur = set( xpr.findall( diff --git a/icepyx/core/APIformatting.py b/icepyx/core/APIformatting.py index b5d31bdfa..55d49f84c 100644 --- a/icepyx/core/APIformatting.py +++ b/icepyx/core/APIformatting.py @@ -205,6 +205,7 @@ class Parameters: """ def __init__(self, partype, values=None, reqtype=None): + assert partype in [ "CMR", "required", diff --git a/icepyx/core/auth.py b/icepyx/core/auth.py index cf771f420..7c36126f9 100644 --- a/icepyx/core/auth.py +++ b/icepyx/core/auth.py @@ -4,16 +4,14 @@ import earthaccess - class AuthenticationError(Exception): - """ + ''' Raised when an error is encountered while authenticating Earthdata credentials - """ - + ''' pass -class EarthdataAuthMixin: +class EarthdataAuthMixin(): """ This mixin class generates the needed authentication sessions and tokens, including for NASA Earthdata cloud access. Authentication is completed using the [earthaccess library](https://nsidc.github.io/earthaccess/). @@ -23,27 +21,26 @@ class EarthdataAuthMixin: 3. Storing credentials in a .netrc file (not recommended for security reasons) More details on using these methods is available in the [earthaccess documentation](https://nsidc.github.io/earthaccess/tutorials/restricted-datasets/#auth). - This class can be inherited by any other class that requires authentication. For - example, the `Query` class inherits this one, and so a Query object has the + This class can be inherited by any other class that requires authentication. For + example, the `Query` class inherits this one, and so a Query object has the `.session` property. The method `earthdata_login()` is included for backwards compatibility. - + The class can be created without any initialization parameters, and the properties will - be populated when they are called. It can alternately be initialized with an - earthaccess.auth.Auth object, which will then be used to create a session or + be populated when they are called. It can alternately be initialized with an + earthaccess.auth.Auth object, which will then be used to create a session or s3login_credentials as they are called. - + Parameters ---------- auth : earthaccess.auth.Auth, default None Optional parameter to initialize an object with existing credentials. - + Examples -------- >>> a = EarthdataAuthMixin() >>> a.session # doctest: +SKIP >>> a.s3login_credentials # doctest: +SKIP """ - def __init__(self, auth=None): self._auth = copy.deepcopy(auth) # initializatin of session and s3 creds is not allowed because those are generated @@ -61,27 +58,25 @@ def __str__(self): @property def auth(self): - """ - Authentication object returned from earthaccess.login() which stores user authentication. - """ + ''' + Authentication object returned from earthaccess.login() which stores user authentication. + ''' # Only login the first time .auth is accessed if self._auth is None: auth = earthaccess.login() # check for a valid auth response if auth.authenticated is False: - raise AuthenticationError( - "Earthdata authentication failed. Check output for error message" - ) + raise AuthenticationError('Earthdata authentication failed. Check output for error message') else: self._auth = auth - + return self._auth @property def session(self): - """ + ''' Earthaccess session object for connecting to Earthdata resources. - """ + ''' # Only generate a session the first time .session is accessed if self._session is None: self._session = self.auth.get_session() @@ -89,26 +84,24 @@ def session(self): @property def s3login_credentials(self): - """ + ''' A dictionary which stores login credentials for AWS s3 access. This property is accessed if using AWS cloud data. - + Because s3 tokens are only good for one hour, this function will automatically check if an hour has elapsed since the last token use and generate a new token if necessary. - """ - + ''' + def set_s3_creds(): - """Store s3login creds from `auth`and reset the last updated timestamp""" + ''' Store s3login creds from `auth`and reset the last updated timestamp''' self._s3login_credentials = self.auth.get_s3_credentials(daac="NSIDC") self._s3_initial_ts = datetime.datetime.now() - + # Only generate s3login_credentials the first time credentials are accessed, or if an hour - # has passed since the last login + # has passed since the last login if self._s3login_credentials is None: set_s3_creds() - elif (datetime.datetime.now() - self._s3_initial_ts) >= datetime.timedelta( - hours=1 - ): + elif (datetime.datetime.now() - self._s3_initial_ts) >= datetime.timedelta(hours=1): set_s3_creds() return self._s3login_credentials @@ -116,7 +109,7 @@ def earthdata_login(self, uid=None, email=None, s3token=None, **kwargs) -> None: """ Authenticate with NASA Earthdata to enable data ordering and download. Credential storage details are described in the EathdataAuthMixin class section. - + **Note:** This method is maintained for backward compatibility. It is no longer required to explicitly run `.earthdata_login()`. Authentication will be performed by the module as needed when `.session` or `.s3login_credentials` are accessed. Parameters @@ -141,14 +134,12 @@ def earthdata_login(self, uid=None, email=None, s3token=None, **kwargs) -> None: """ warnings.warn( - "It is no longer required to explicitly run the `.earthdata_login()` method. Authentication will be performed by the module as needed.", - DeprecationWarning, - stacklevel=2, - ) - + "It is no longer required to explicitly run the `.earthdata_login()` method. Authentication will be performed by the module as needed.", + DeprecationWarning, stacklevel=2 + ) + if uid != None or email != None or s3token != None: warnings.warn( "The user id (uid) and/or email keyword arguments are no longer required.", - DeprecationWarning, - stacklevel=2, + DeprecationWarning, stacklevel=2 ) diff --git a/icepyx/core/exceptions.py b/icepyx/core/exceptions.py index d20bbfe61..a36a1b645 100644 --- a/icepyx/core/exceptions.py +++ b/icepyx/core/exceptions.py @@ -2,7 +2,6 @@ class DeprecationError(Exception): """ Class raised for use of functionality that is no longer supported by icepyx. """ - pass @@ -28,3 +27,5 @@ def __init__( def __str__(self): return f"{self.msgtxt}: {self.errmsg}" + + diff --git a/icepyx/core/icesat2data.py b/icepyx/core/icesat2data.py index aa35fd433..cebce4160 100644 --- a/icepyx/core/icesat2data.py +++ b/icepyx/core/icesat2data.py @@ -2,9 +2,8 @@ class Icesat2Data: - def __init__( - self, - ): + def __init__(self,): + warnings.filterwarnings("always") warnings.warn( "DEPRECATED. Please use icepyx.Query to create a download data object (all other functionality is the same)", diff --git a/icepyx/core/query.py b/icepyx/core/query.py index d857bbb3d..4ffe4c241 100644 --- a/icepyx/core/query.py +++ b/icepyx/core/query.py @@ -351,9 +351,9 @@ class Query(GenQuery, EarthdataAuthMixin): files : string, default None A placeholder for future development. Not used for any purposes yet. auth : earthaccess.auth.Auth, default None - An earthaccess authentication object. Available as an argument so an existing - earthaccess.auth.Auth object can be used for authentication. If not given, a new auth - object will be created whenever authentication is needed. + An earthaccess authentication object. Available as an argument so an existing + earthaccess.auth.Auth object can be used for authentication. If not given, a new auth + object will be created whenever authentication is needed. Returns ------- @@ -411,6 +411,7 @@ def __init__( auth=None, **kwargs, ): + # Check necessary combination of input has been specified if ( (product is None or spatial_extent is None) diff --git a/icepyx/core/spatial.py b/icepyx/core/spatial.py index c34e928ed..7702acdf2 100644 --- a/icepyx/core/spatial.py +++ b/icepyx/core/spatial.py @@ -80,6 +80,7 @@ def geodataframe(extent_type, spatial_extent, file=False, xdateline=None): # DevGoal: the crs setting and management needs to be improved elif extent_type == "polygon" and file == False: + # if spatial_extent is already a Polygon if isinstance(spatial_extent, Polygon): spatial_extent_geom = spatial_extent @@ -247,6 +248,7 @@ def validate_polygon_pairs(spatial_extent): if (spatial_extent[0][0] != spatial_extent[-1][0]) or ( spatial_extent[0][1] != spatial_extent[-1][1] ): + # Throw a warning warnings.warn( "WARNING: Polygon's first and last point's coordinates differ," @@ -434,6 +436,7 @@ def __init__(self, spatial_extent, **kwarg): # Check if spatial_extent is a list of coordinates (bounding box or polygon) if isinstance(spatial_extent, (list, np.ndarray)): + # bounding box if len(spatial_extent) == 4 and all( isinstance(i, scalar_types) for i in spatial_extent diff --git a/icepyx/core/temporal.py b/icepyx/core/temporal.py index 67f59882a..c7e2dda1c 100644 --- a/icepyx/core/temporal.py +++ b/icepyx/core/temporal.py @@ -51,6 +51,7 @@ def convert_string_to_date(date): def check_valid_date_range(start, end): + """ Helper function for checking if a date range is valid. @@ -88,6 +89,7 @@ def check_valid_date_range(start, end): def validate_times(start_time, end_time): + """ Validates the start and end times passed into __init__ and returns them as datetime.time objects. @@ -143,6 +145,7 @@ def validate_times(start_time, end_time): def validate_date_range_datestr(date_range, start_time=None, end_time=None): + """ Validates a date range provided in the form of a list of strings. @@ -187,6 +190,7 @@ def validate_date_range_datestr(date_range, start_time=None, end_time=None): def validate_date_range_datetime(date_range, start_time=None, end_time=None): + """ Validates a date range provided in the form of a list of datetimes. @@ -226,6 +230,7 @@ def validate_date_range_datetime(date_range, start_time=None, end_time=None): def validate_date_range_date(date_range, start_time=None, end_time=None): + """ Validates a date range provided in the form of a list of datetime.date objects. @@ -263,6 +268,7 @@ def validate_date_range_date(date_range, start_time=None, end_time=None): def validate_date_range_dict(date_range, start_time=None, end_time=None): + """ Validates a date range provided in the form of a dict with the following keys: @@ -324,6 +330,7 @@ def validate_date_range_dict(date_range, start_time=None, end_time=None): # if is string date elif isinstance(_start_date, str): + _start_date = convert_string_to_date(_start_date) _start_date = dt.datetime.combine(_start_date, start_time) @@ -404,6 +411,7 @@ def __init__(self, date_range, start_time=None, end_time=None): """ if len(date_range) == 2: + # date range is provided as dict of strings, dates, or datetimes if isinstance(date_range, dict): self._start, self._end = validate_date_range_dict( diff --git a/icepyx/core/validate_inputs.py b/icepyx/core/validate_inputs.py index a69f045fb..d74768eea 100644 --- a/icepyx/core/validate_inputs.py +++ b/icepyx/core/validate_inputs.py @@ -105,17 +105,15 @@ def tracks(track): return track_list - def check_s3bucket(path): """ Check if the given path is an s3 path. Raise a warning if the data being referenced is not in the NSIDC bucket """ - split_path = path.split("/") - if split_path[0] == "s3:" and split_path[2] != "nsidc-cumulus-prod-protected": + split_path = path.split('/') + if split_path[0] == 's3:' and split_path[2] != 'nsidc-cumulus-prod-protected': warnings.warn( - "s3 data being read from outside the NSIDC data bucket. Icepyx can " - "read this data, but available data lists may not be accurate.", - stacklevel=2, + 's3 data being read from outside the NSIDC data bucket. Icepyx can ' + 'read this data, but available data lists may not be accurate.', stacklevel=2 ) return path diff --git a/icepyx/core/variables.py b/icepyx/core/variables.py index 4dd5444fe..4c52003df 100644 --- a/icepyx/core/variables.py +++ b/icepyx/core/variables.py @@ -29,7 +29,7 @@ class Variables(EarthdataAuthMixin): contained in ICESat-2 products. Parameters - ---------- + ---------- vartype : string This argument is deprecated. The vartype will be inferred from data_source. One of ['order', 'file'] to indicate the source of the input variables. @@ -49,9 +49,9 @@ class Variables(EarthdataAuthMixin): wanted : dictionary, default None As avail, but for the desired list of variables auth : earthaccess.auth.Auth, default None - An earthaccess authentication object. Available as an argument so an existing - earthaccess.auth.Auth object can be used for authentication. If not given, a new auth - object will be created whenever authentication is needed. + An earthaccess authentication object. Available as an argument so an existing + earthaccess.auth.Auth object can be used for authentication. If not given, a new auth + object will be created whenever authentication is needed. """ def __init__( @@ -65,28 +65,28 @@ def __init__( auth=None, ): # Deprecation error - if vartype in ["order", "file"]: + if vartype in ['order', 'file']: raise DeprecationError( - "It is no longer required to specify the variable type `vartype`. Instead please ", - "provide either the path to a local file (arg: `path`) or the product you would ", - "like variables for (arg: `product`).", + 'It is no longer required to specify the variable type `vartype`. Instead please ', + 'provide either the path to a local file (arg: `path`) or the product you would ', + 'like variables for (arg: `product`).' ) - + if path and product: raise TypeError( - "Please provide either a path or a product. If a path is provided ", - "variables will be read from the file. If a product is provided all available ", - "variables for that product will be returned.", + 'Please provide either a path or a product. If a path is provided ', + 'variables will be read from the file. If a product is provided all available ', + 'variables for that product will be returned.' ) # initialize authentication properties EarthdataAuthMixin.__init__(self, auth=auth) - + # Set the product and version from either the input args or the file if path: self._path = val.check_s3bucket(path) # Set up auth - if self._path.startswith("s3"): + if self._path.startswith('s3'): auth = self.auth else: auth = None @@ -98,19 +98,15 @@ def __init__( self._product = is2ref._validate_product(product) # Check for valid version string # If version is not specified by the user assume the most recent version - self._version = val.prod_version( - is2ref.latest_version(self._product), version - ) + self._version = val.prod_version(is2ref.latest_version(self._product), version) else: - raise TypeError( - "Either a path or a product need to be given as input arguments." - ) - + raise TypeError('Either a path or a product need to be given as input arguments.') + self._avail = avail self.wanted = wanted # DevGoal: put some more/robust checks here to assess validity of inputs - + @property def path(self): if self._path: @@ -118,14 +114,15 @@ def path(self): else: path = None return path - + @property def product(self): return self._product - + @property def version(self): return self._version + def avail(self, options=False, internal=False): """ @@ -146,7 +143,7 @@ def avail(self, options=False, internal=False): """ if not hasattr(self, "_avail") or self._avail == None: - if not hasattr(self, "path") or self.path.startswith("s3"): + if not hasattr(self, 'path') or self.path.startswith('s3'): self._avail = is2ref._get_custom_options( self.session, self.product, self.version )["variables"] @@ -631,6 +628,7 @@ def remove(self, all=False, var_list=None, beam_list=None, keyword_list=None): for bkw in beam_list: if bkw in vpath_kws: for kw in keyword_list: + if kw in vpath_kws: self.wanted[vkey].remove(vpath) except TypeError: diff --git a/icepyx/core/visualization.py b/icepyx/core/visualization.py index 001ae178e..32c81e3e7 100644 --- a/icepyx/core/visualization.py +++ b/icepyx/core/visualization.py @@ -142,6 +142,7 @@ def __init__( cycles=None, tracks=None, ): + if query_obj: pass else: @@ -240,6 +241,7 @@ def query_icesat2_filelist(self) -> tuple: is2_file_list = [] for bbox_i in bbox_list: + try: region = ipx.Query( self.product, @@ -362,6 +364,7 @@ def request_OA_data(self, paras) -> da.array: # get data we need (with the correct date) try: + df_series = df.query(expr="date == @Date").iloc[0] beam_data = df_series.beams @@ -480,6 +483,7 @@ def viz_elevation(self) -> (hv.DynamicMap, hv.Layout): return (None,) * 2 else: + cols = ( ["lat", "lon", "elevation", "canopy", "rgt", "cycle"] if self.product == "ATL08" diff --git a/icepyx/quest/dataset_scripts/__init__.py b/icepyx/quest/dataset_scripts/__init__.py index 7834127ff..c7b28ee49 100644 --- a/icepyx/quest/dataset_scripts/__init__.py +++ b/icepyx/quest/dataset_scripts/__init__.py @@ -1 +1 @@ -from .dataset import * +from .dataset import * \ No newline at end of file diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 00a57a9f3..df67c4b1b 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -46,6 +46,7 @@ def __init__(self, aoi, toi, params=["temperature"], presRange=None): self._apikey = "92259861231b55d32a9c0e4e3a93f4834fc0b6fa" def __str__(self): + if self.presRange is None: prange = "All" else: @@ -477,7 +478,7 @@ def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFr profile_df = self._parse_into_df(profile_data[0]) merged_df = pd.concat([merged_df, profile_df], sort=False) except: - print("\tError processing profile {0}. Skipping.".format(i)) + print('\tError processing profile {0}. Skipping.'.format(i)) # now that we have a df from this round of downloads, we can add it to any existing dataframe # note that if a given column has previously been added, update needs to be used to replace nans (merge will not replace the nan values) @@ -502,6 +503,7 @@ def save(self, filepath): e.g. /path/to/file/my_data(.csv) """ + # create the directory if it doesn't exist path, file = os.path.split(filepath) if not os.path.exists(path): @@ -509,4 +511,4 @@ def save(self, filepath): # remove file extension base, ext = os.path.splitext(filepath) - self.argodata.to_csv(base + "_argo.csv") + self.argodata.to_csv(base + '_argo.csv') diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index 193fab22e..8990686f9 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -11,6 +11,7 @@ class DataSet: All sub-classes must support the following methods for use via the QUEST class. """ + def __init__(self, spatial_extent, date_range, start_time=None, end_time=None): """ Complete any dataset specific initializations (i.e. beyond space and time) required here. diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index 3b8620681..b919552fa 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -217,6 +217,7 @@ def download_all(self, path="", **kwargs): print() try: + if isinstance(v, Query): print("---ICESat-2---") try: @@ -235,7 +236,9 @@ def download_all(self, path="", **kwargs): dataset_name = type(v).__name__ print("Error downloading data from {0}".format(dataset_name)) + def save_all(self, path): + for k, v in self.datasets.items(): if isinstance(v, Query): print("ICESat-2 granules are saved during download") diff --git a/icepyx/tests/conftest.py b/icepyx/tests/conftest.py index 9ce8e4081..fca31847a 100644 --- a/icepyx/tests/conftest.py +++ b/icepyx/tests/conftest.py @@ -2,7 +2,6 @@ import pytest from unittest import mock - # PURPOSE: mock environmental variables @pytest.fixture(scope="session", autouse=True) def mock_settings_env_vars(): diff --git a/icepyx/tests/test_APIformatting.py b/icepyx/tests/test_APIformatting.py index 213c1cf8a..83e88a131 100644 --- a/icepyx/tests/test_APIformatting.py +++ b/icepyx/tests/test_APIformatting.py @@ -11,7 +11,6 @@ # CMR temporal and spatial formats --> what's the best way to compare formatted text? character by character comparison of strings? - ########## _fmt_temporal ########## def test_time_fmt(): obs = apifmt._fmt_temporal( diff --git a/icepyx/tests/test_Earthdata.py b/icepyx/tests/test_Earthdata.py index 60b92f621..8ad883e6a 100644 --- a/icepyx/tests/test_Earthdata.py +++ b/icepyx/tests/test_Earthdata.py @@ -8,7 +8,6 @@ import shutil import warnings - # PURPOSE: test different authentication methods @pytest.fixture(scope="module", autouse=True) def setup_earthdata(): @@ -66,6 +65,7 @@ def earthdata_login(uid=None, pwd=None, email=None, s3token=False) -> bool: url = "urs.earthdata.nasa.gov" mock_uid, _, mock_pwd = netrc.netrc(netrc).authenticators(url) except: + mock_uid = os.environ.get("EARTHDATA_USERNAME") mock_pwd = os.environ.get("EARTHDATA_PASSWORD") diff --git a/icepyx/tests/test_auth.py b/icepyx/tests/test_auth.py index 8507b1e40..6ac77c864 100644 --- a/icepyx/tests/test_auth.py +++ b/icepyx/tests/test_auth.py @@ -8,35 +8,30 @@ @pytest.fixture() def auth_instance(): - """ + ''' An EarthdatAuthMixin object for each of the tests. Default scope is function level, so a new instance should be created for each of the tests. - """ + ''' return EarthdataAuthMixin() - # Test that .session creates a session def test_get_session(auth_instance): assert isinstance(auth_instance.session, requests.sessions.Session) - # Test that .s3login_credentials creates a dict with the correct keys def test_get_s3login_credentials(auth_instance): assert isinstance(auth_instance.s3login_credentials, dict) - expected_keys = set( - ["accessKeyId", "secretAccessKey", "sessionToken", "expiration"] - ) + expected_keys = set(['accessKeyId', 'secretAccessKey', 'sessionToken', + 'expiration']) assert set(auth_instance.s3login_credentials.keys()) == expected_keys - # Test that earthdata_login generates an auth object def test_login_function(auth_instance): auth_instance.earthdata_login() assert isinstance(auth_instance.auth, earthaccess.auth.Auth) assert auth_instance.auth.authenticated - # Test that earthdata_login raises a warning if email is provided def test_depreciation_warning(auth_instance): with pytest.warns(DeprecationWarning): - auth_instance.earthdata_login(email="me@gmail.com") + auth_instance.earthdata_login(email='me@gmail.com') diff --git a/icepyx/tests/test_query.py b/icepyx/tests/test_query.py index 15eebfcbd..7738c424a 100644 --- a/icepyx/tests/test_query.py +++ b/icepyx/tests/test_query.py @@ -9,7 +9,6 @@ # seem to be adequately covered in docstrings; # may want to focus on testing specific queries - # ------------------------------------ # icepyx-specific tests # ------------------------------------ diff --git a/icepyx/tests/test_quest.py b/icepyx/tests/test_quest.py index 0ba7325a6..8746718a8 100644 --- a/icepyx/tests/test_quest.py +++ b/icepyx/tests/test_quest.py @@ -15,7 +15,6 @@ def quest_instance(scope="module", autouse=True): ########## PER-DATASET ADDITION TESTS ########## - # Paramaterize these add_dataset tests once more datasets are added def test_add_is2(quest_instance): # Add ATL06 as a test to QUEST @@ -60,7 +59,6 @@ def test_add_multiple_datasets(quest_instance): ########## ALL DATASET METHODS TESTS ########## - # each of the query functions should be tested in their respective modules def test_search_all(quest_instance): quest_instance.add_argo(params=["down_irradiance412", "temperature"]) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index a6940fe7b..3497da672 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -5,7 +5,6 @@ from icepyx.quest.quest import Quest - # create an Argo instance via quest (Argo is a submodule) @pytest.fixture(scope="function") def argo_quest_instance(): @@ -161,18 +160,17 @@ def test_download_parse_into_df(argo_quest_instance): # approach for additional testing of df functions: create json files with profiles and store them in test suite # then use those for the comparison (e.g. number of rows in df and json match) - def test_save_df_to_csv(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) reg_a.download() # note: pressure is returned by default + path = os.getcwd() + "test_file" reg_a.save(path) assert os.path.exists(path + "_argo.csv") os.remove(path + "_argo.csv") - def test_merge_df(argo_quest_instance): reg_a = argo_quest_instance([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) param_list = ["salinity", "temperature", "down_irradiance412"] @@ -189,10 +187,11 @@ def test_merge_df(argo_quest_instance): assert "down_irradiance412_argoqc" in df.columns + + # --------------------------------------------------- # Test kwargs to replace params and presRange in search and download - def test_replace_param_search(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) diff --git a/icepyx/tests/test_read.py b/icepyx/tests/test_read.py index d6727607e..018435968 100644 --- a/icepyx/tests/test_read.py +++ b/icepyx/tests/test_read.py @@ -21,6 +21,7 @@ def test_check_datasource_type(): ], ) def test_check_datasource(filepath, expect): + source_type = read._check_datasource(filepath) assert source_type == expect @@ -89,6 +90,7 @@ def test_validate_source_str_not_a_dir_or_file(): ], ) def test_check_run_fast_scandir(dir, fn_glob, expect): + (subfolders, files) = read._run_fast_scandir(dir, fn_glob) assert (sorted(subfolders), sorted(files)) == expect diff --git a/icepyx/tests/test_spatial.py b/icepyx/tests/test_spatial.py index 4d6369d9e..2666d857d 100644 --- a/icepyx/tests/test_spatial.py +++ b/icepyx/tests/test_spatial.py @@ -351,6 +351,7 @@ def test_poly_list_auto_close(): def test_poly_file_simple_one_poly(): + poly_from_file = spat.Spatial( str( Path( @@ -390,6 +391,7 @@ def test_bad_poly_inputfile_type_throws_error(): def test_gdf_from_one_bbox(): + obs = spat.geodataframe("bounding_box", [-55, 68, -48, 71]) geom = [Polygon(list(zip([-55, -55, -48, -48, -55], [68, 71, 71, 68, 68])))] exp = gpd.GeoDataFrame(geometry=geom) diff --git a/icepyx/tests/test_temporal.py b/icepyx/tests/test_temporal.py index c93b30a38..83926946e 100644 --- a/icepyx/tests/test_temporal.py +++ b/icepyx/tests/test_temporal.py @@ -235,7 +235,6 @@ def test_range_str_yyyydoy_dict_time_start_end(): # Date Range Errors - # (The following inputs are bad, testing to ensure the temporal class handles this elegantly) def test_bad_start_time_type(): with pytest.raises(AssertionError): diff --git a/icepyx/tests/test_visualization.py b/icepyx/tests/test_visualization.py index dfd41116f..0a1f2fa43 100644 --- a/icepyx/tests/test_visualization.py +++ b/icepyx/tests/test_visualization.py @@ -62,6 +62,7 @@ def test_files_in_latest_cycles(n, exp): ], ) def test_gran_paras(filename, expect): + para_list = vis.gran_paras(filename) assert para_list == expect From cc8b1d0025a1c9af9841f2d34c285f3fbe472d51 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 6 Dec 2023 11:44:54 -0500 Subject: [PATCH 118/124] run black formatter on files in this PR --- icepyx/quest/dataset_scripts/__init__.py | 2 +- icepyx/quest/dataset_scripts/argo.py | 6 ++---- icepyx/quest/dataset_scripts/dataset.py | 1 - icepyx/quest/quest.py | 3 --- icepyx/tests/test_quest.py | 2 ++ icepyx/tests/test_quest_argo.py | 7 ++++--- 6 files changed, 9 insertions(+), 12 deletions(-) diff --git a/icepyx/quest/dataset_scripts/__init__.py b/icepyx/quest/dataset_scripts/__init__.py index c7b28ee49..7834127ff 100644 --- a/icepyx/quest/dataset_scripts/__init__.py +++ b/icepyx/quest/dataset_scripts/__init__.py @@ -1 +1 @@ -from .dataset import * \ No newline at end of file +from .dataset import * diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index df67c4b1b..00a57a9f3 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -46,7 +46,6 @@ def __init__(self, aoi, toi, params=["temperature"], presRange=None): self._apikey = "92259861231b55d32a9c0e4e3a93f4834fc0b6fa" def __str__(self): - if self.presRange is None: prange = "All" else: @@ -478,7 +477,7 @@ def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFr profile_df = self._parse_into_df(profile_data[0]) merged_df = pd.concat([merged_df, profile_df], sort=False) except: - print('\tError processing profile {0}. Skipping.'.format(i)) + print("\tError processing profile {0}. Skipping.".format(i)) # now that we have a df from this round of downloads, we can add it to any existing dataframe # note that if a given column has previously been added, update needs to be used to replace nans (merge will not replace the nan values) @@ -503,7 +502,6 @@ def save(self, filepath): e.g. /path/to/file/my_data(.csv) """ - # create the directory if it doesn't exist path, file = os.path.split(filepath) if not os.path.exists(path): @@ -511,4 +509,4 @@ def save(self, filepath): # remove file extension base, ext = os.path.splitext(filepath) - self.argodata.to_csv(base + '_argo.csv') + self.argodata.to_csv(base + "_argo.csv") diff --git a/icepyx/quest/dataset_scripts/dataset.py b/icepyx/quest/dataset_scripts/dataset.py index 8990686f9..193fab22e 100644 --- a/icepyx/quest/dataset_scripts/dataset.py +++ b/icepyx/quest/dataset_scripts/dataset.py @@ -11,7 +11,6 @@ class DataSet: All sub-classes must support the following methods for use via the QUEST class. """ - def __init__(self, spatial_extent, date_range, start_time=None, end_time=None): """ Complete any dataset specific initializations (i.e. beyond space and time) required here. diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index b919552fa..3b8620681 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -217,7 +217,6 @@ def download_all(self, path="", **kwargs): print() try: - if isinstance(v, Query): print("---ICESat-2---") try: @@ -236,9 +235,7 @@ def download_all(self, path="", **kwargs): dataset_name = type(v).__name__ print("Error downloading data from {0}".format(dataset_name)) - def save_all(self, path): - for k, v in self.datasets.items(): if isinstance(v, Query): print("ICESat-2 granules are saved during download") diff --git a/icepyx/tests/test_quest.py b/icepyx/tests/test_quest.py index 8746718a8..0ba7325a6 100644 --- a/icepyx/tests/test_quest.py +++ b/icepyx/tests/test_quest.py @@ -15,6 +15,7 @@ def quest_instance(scope="module", autouse=True): ########## PER-DATASET ADDITION TESTS ########## + # Paramaterize these add_dataset tests once more datasets are added def test_add_is2(quest_instance): # Add ATL06 as a test to QUEST @@ -59,6 +60,7 @@ def test_add_multiple_datasets(quest_instance): ########## ALL DATASET METHODS TESTS ########## + # each of the query functions should be tested in their respective modules def test_search_all(quest_instance): quest_instance.add_argo(params=["down_irradiance412", "temperature"]) diff --git a/icepyx/tests/test_quest_argo.py b/icepyx/tests/test_quest_argo.py index 3497da672..a6940fe7b 100644 --- a/icepyx/tests/test_quest_argo.py +++ b/icepyx/tests/test_quest_argo.py @@ -5,6 +5,7 @@ from icepyx.quest.quest import Quest + # create an Argo instance via quest (Argo is a submodule) @pytest.fixture(scope="function") def argo_quest_instance(): @@ -160,17 +161,18 @@ def test_download_parse_into_df(argo_quest_instance): # approach for additional testing of df functions: create json files with profiles and store them in test suite # then use those for the comparison (e.g. number of rows in df and json match) + def test_save_df_to_csv(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-13"]) reg_a.download() # note: pressure is returned by default - path = os.getcwd() + "test_file" reg_a.save(path) assert os.path.exists(path + "_argo.csv") os.remove(path + "_argo.csv") + def test_merge_df(argo_quest_instance): reg_a = argo_quest_instance([-150, 30, -120, 60], ["2022-06-07", "2022-06-14"]) param_list = ["salinity", "temperature", "down_irradiance412"] @@ -187,11 +189,10 @@ def test_merge_df(argo_quest_instance): assert "down_irradiance412_argoqc" in df.columns - - # --------------------------------------------------- # Test kwargs to replace params and presRange in search and download + def test_replace_param_search(argo_quest_instance): reg_a = argo_quest_instance([-154, 30, -143, 37], ["2022-04-12", "2022-04-26"]) From d467a847452386f7bdd0e8e76559179eac544822 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 6 Dec 2023 11:47:22 -0500 Subject: [PATCH 119/124] Update doc/source/contributing/quest-available-datasets.rst --- doc/source/contributing/quest-available-datasets.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/contributing/quest-available-datasets.rst b/doc/source/contributing/quest-available-datasets.rst index fbc563a7f..86901f7ed 100644 --- a/doc/source/contributing/quest-available-datasets.rst +++ b/doc/source/contributing/quest-available-datasets.rst @@ -23,6 +23,7 @@ Adding a Dataset to QUEST Want to add a new dataset to QUEST? No problem! QUEST includes a template script (``dataset.py``) that may be used to create your own querying module for a dataset of interest. -Once you have developed a script with the template, you may request for the module to be added to QUEST via Github. Please see the How to Contribute page :ref:`dev_guide_label` for instructions on how to contribute to icepyx. +Once you have developed a script with the template, you may request for the module to be added to QUEST via GitHub. +Please see the How to Contribute page :ref:`dev_guide_label` for instructions on how to contribute to icepyx. Detailed guidelines on how to construct your dataset module are currently a work in progress. From fc9e086facc6f78548c3c1ca7880137fede0ebf6 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 6 Dec 2023 12:48:37 -0500 Subject: [PATCH 120/124] add docstring for new save_all fn --- icepyx/quest/quest.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index 3b8620681..2f736d664 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -14,19 +14,16 @@ class Quest(GenQuery): See the doc page for GenQuery for details on temporal and spatial input parameters. - Parameters ---------- proj : proj4 string Geospatial projection. Not yet implemented - Returns ------- quest object - Examples -------- Initializing Quest with a bounding box. @@ -167,11 +164,11 @@ def search_all(self, **kwargs): Parameters ---------- **kwargs : default None - Optional passing of keyword arguments to supply additional search constraints per datasets. - Each key must match the dataset name (e.g. "icesat2", "argo") as in quest.datasets.keys(), - and the value is a dictionary of acceptable keyword arguments - and values allowable for the `search_data()` function for that dataset. - For instance: `icesat2 = {"IDs":True}, argo = {"presRange":"10,500"}`. + Optional passing of keyword arguments to supply additional search constraints per datasets. + Each key must match the dataset name (e.g. "icesat2", "argo") as in quest.datasets.keys(), + and the value is a dictionary of acceptable keyword arguments + and values allowable for the `search_data()` function for that dataset. + For instance: `icesat2 = {"IDs":True}, argo = {"presRange":"10,500"}`. """ print("\nSearching all datasets...") @@ -204,11 +201,11 @@ def download_all(self, path="", **kwargs): Parameters ---------- **kwargs : default None - Optional passing of keyword arguments to supply additional search constraints per datasets. - Each key must match the dataset name (e.g. "icesat2", "argo") as in quest.datasets.keys(), - and the value is a dictionary of acceptable keyword arguments - and values allowable for the `search_data()` function for that dataset. - For instance: `icesat2 = {"verbose":True}, argo = {"keep_existing":True}`. + Optional passing of keyword arguments to supply additional search constraints per datasets. + Each key must match the dataset name (e.g. "icesat2", "argo") as in quest.datasets.keys(), + and the value is a dictionary of acceptable keyword arguments + and values allowable for the `search_data()` function for that dataset. + For instance: `icesat2 = {"verbose":True}, argo = {"keep_existing":True}`. """ print("\nDownloading all datasets...") @@ -236,6 +233,16 @@ def download_all(self, path="", **kwargs): print("Error downloading data from {0}".format(dataset_name)) def save_all(self, path): + """ + Saves all datasets according to their respective `.save()` functionality. + + Parameters + ---------- + path : str + Path at which to save the dataset files. + + """ + for k, v in self.datasets.items(): if isinstance(v, Query): print("ICESat-2 granules are saved during download") From 54a14eab32c6707ff5b3163e36eba9d9ca5f1898 Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 6 Dec 2023 12:51:00 -0500 Subject: [PATCH 121/124] remove savename kwarg --- icepyx/quest/quest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/icepyx/quest/quest.py b/icepyx/quest/quest.py index 2f736d664..966b19dca 100644 --- a/icepyx/quest/quest.py +++ b/icepyx/quest/quest.py @@ -224,9 +224,9 @@ def download_all(self, path="", **kwargs): else: print(k) try: - msg = v.download(kwargs[k], savename=path) + msg = v.download(kwargs[k]) except KeyError: - msg = v.download(savename=path) + msg = v.download() print(msg) except: dataset_name = type(v).__name__ From ae1d484abeb5b2e8cb8e62831833b1a82582bb1d Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 6 Dec 2023 13:07:44 -0500 Subject: [PATCH 122/124] standardize docstring spaces in argo.py --- icepyx/quest/dataset_scripts/argo.py | 43 +++++++++++++++------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/icepyx/quest/dataset_scripts/argo.py b/icepyx/quest/dataset_scripts/argo.py index 00a57a9f3..8c614d301 100644 --- a/icepyx/quest/dataset_scripts/argo.py +++ b/icepyx/quest/dataset_scripts/argo.py @@ -15,16 +15,16 @@ class Argo(DataSet): Parameters --------- - aoi: + aoi : area of interest supplied via the spatial parameter of the QUEST object - toi: + toi : time period of interest supplied via the temporal parameter of the QUEST object - params: list of str, default ["temperature"] + params : list of str, default ["temperature"] A list of strings, where each string is a requested parameter. Only metadata for profiles with the requested parameters are returned. To search for all parameters, use `params=["all"]`; be careful using all for floats with BGC data, as this may be result in a large download. - presRange: str, default None + presRange : str, default None The pressure range (which correllates with depth) to search for data within. Input as a "shallow-limit,deep-limit" string. @@ -35,7 +35,6 @@ class Argo(DataSet): # Note: it looks like ArgoVis now accepts polygons, not just bounding boxes def __init__(self, aoi, toi, params=["temperature"], presRange=None): - # super().__init__(boundingbox, timeframe) self._params = self._validate_parameters(params) self._presRange = presRange self._spatial = aoi @@ -82,6 +81,7 @@ def params(self, value): """ Validate the input list of parameters. """ + self._params = list(set(self._validate_parameters(value))) @property @@ -134,6 +134,7 @@ def _valid_params(self) -> list: To get a list of valid parameters, comment out the validation line in `search_data` herein, submit a search with an invalid parameter, and get the list from the response. """ + valid_params = [ # all argo "pressure", @@ -246,24 +247,24 @@ def search_data(self, params=None, presRange=None, printURL=False) -> str: Parameters --------- - params: list of str, default None + params : list of str, default None A list of strings, where each string is a requested parameter. This kwarg is used to replace the existing list in `self.params`. Do not submit this kwarg if you would like to use the existing `self.params` list. Only metadata for profiles with the requested parameters are returned. To search for all parameters, use `params=["all"]`; be careful using all for floats with BGC data, as this may be result in a large download. - presRange: str, default None + presRange : str, default None The pressure range (which correllates with depth) to search for data within. This kwarg is used to replace the existing pressure range in `self.presRange`. Do not submit this kwarg if you would like to use the existing `self.presRange` values. Input as a "shallow-limit,deep-limit" string. - printURL: boolean, default False + printURL : boolean, default False Print the URL of the data request. Useful for debugging and when no data is returned. Returns ------ - str: message on the success status of the search + str : message on the success status of the search """ # if search is called with replaced parameters or presRange @@ -339,7 +340,7 @@ def _download_profile( Returns ------ - dict: json formatted dictionary of the profile data + dict : json formatted dictionary of the profile data """ # builds URL to be submitted @@ -380,7 +381,7 @@ def _parse_into_df(self, profile_data) -> pd.DataFrame: Returns ------- - pandas DataFrame of the profile data + pd.DataFrame : DataFrame of profile data """ profileDf = pd.DataFrame( @@ -415,24 +416,24 @@ def download(self, params=None, presRange=None, keep_existing=True) -> pd.DataFr Parameters ---------- - params: list of str, default None + params : list of str, default None A list of strings, where each string is a requested parameter. This kwarg is used to replace the existing list in `self.params`. Do not submit this kwarg if you would like to use the existing `self.params` list. Only metadata for profiles with the requested parameters are returned. To search for all parameters, use `params=["all"]`. For a list of available parameters, see: `reg._valid_params` - presRange: str, default None + presRange : str, default None The pressure range (which correllates with depth) to search for data within. This kwarg is used to replace the existing pressure range in `self.presRange`. Do not submit this kwarg if you would like to use the existing `self.presRange` values. Input as a "shallow-limit,deep-limit" string. - keep_existing: boolean, default True + keep_existing : boolean, default True Provides the option to clear any existing downloaded data before downloading more. Returns ------- - pd.DataFrame: DataFrame of requested data + pd.DataFrame : DataFrame of requested data """ # TODO: do some basic testing of this block and how the dataframe merging actually behaves @@ -496,10 +497,11 @@ def save(self, filepath): Parameters ---------- - filepath: string containing complete filepath and name of file - extension will be removed and replaced with csv. Also appends - '_argo.csv' to filename - e.g. /path/to/file/my_data(.csv) + filepath : str + String containing complete filepath and name of file + Any extension will be removed and replaced with csv. + Also appends '_argo.csv' to filename + e.g. /path/to/file/my_data(_argo.csv) """ # create the directory if it doesn't exist @@ -507,6 +509,7 @@ def save(self, filepath): if not os.path.exists(path): os.mkdir(path) - # remove file extension + # remove any file extension base, ext = os.path.splitext(filepath) + self.argodata.to_csv(base + "_argo.csv") From 468f3ba16773a92aa62e81a569c2da3cccee67eb Mon Sep 17 00:00:00 2001 From: Jessica Scheick Date: Wed, 6 Dec 2023 13:45:32 -0500 Subject: [PATCH 123/124] last updates to notebook --- .../QUEST_argo_data_access.ipynb | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb index 40094d4ee..1bdb5fd0c 100644 --- a/doc/source/example_notebooks/QUEST_argo_data_access.ipynb +++ b/doc/source/example_notebooks/QUEST_argo_data_access.ipynb @@ -23,14 +23,9 @@ "source": [ "# Basic packages\n", "import geopandas as gpd\n", - "import h5py\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "from os import listdir\n", - "from os.path import isfile, join\n", - "import pandas as pd\n", "from pprint import pprint\n", - "import requests\n", "\n", "# icepyx and QUEST\n", "import icepyx as ipx" @@ -92,8 +87,8 @@ "source": [ "## Getting the data\n", "\n", - "Let's first grab the ICESat-2 data. If we want to extract information about the water column, the ATL03 product is likely the desired choice.\n", - "* `short_name`: ATL03 data only" + "Let's first query the ICESat-2 data. If we want to extract information about the water column, the ATL03 product is likely the desired choice.\n", + "* `short_name`: ATL03" ] }, { @@ -142,7 +137,7 @@ "user_expressions": [] }, "source": [ - "Note the ICESat-2 functions shown here are the same as those used for direct icepyx queries. The user is referred to other example workbooks for detailed explanations about additional icepyx features.\n", + "Note the ICESat-2 functions shown here are the same as those used for direct icepyx queries. The user is referred to other [example workbooks](https://icepyx.readthedocs.io/en/latest/example_notebooks/IS2_data_access.html) for detailed explanations about icepyx functionality.\n", "\n", "Accessing ICESat-2 data requires Earthdata login credentials. When running the `download_all()` function below, an authentication check will be passed when attempting to download the ICESat-2 files." ] @@ -202,7 +197,7 @@ "user_expressions": [] }, "source": [ - "Additionally, a user may view or update the list of requested Argo and Argo-BGC parameters at any time through `reg_a.datasets['argo'].params`. If a user submits an invalid parameter (\"temp\" instead of \"temperature\", for example), an `AssertionError` will be passed. `reg_a.datasets['argo'].presRange` behaves anologously for limiting the pressure range of Argo data." + "Additionally, a user may view or update the list of requested Argo and Argo-BGC parameters at any time through `reg_a.datasets['argo'].params`. If a user submits an invalid parameter (\"temp\" instead of \"temperature\", for example), an `AssertionError` will be raised. `reg_a.datasets['argo'].presRange` behaves anologously for limiting the pressure range of Argo data." ] }, { @@ -226,7 +221,7 @@ "user_expressions": [] }, "source": [ - "As for ICESat-2 data, the user can interact directly with the Argo data object (`reg_a.datasets['argo']`) to directly search or download data outside of the `Quest.search_all()` and `Quest.download_all()` functionality shown below.\n", + "As for ICESat-2 data, the user can interact directly with the Argo data object (`reg_a.datasets['argo']`) to search or download data outside of the `Quest.search_all()` and `Quest.download_all()` functionality shown below.\n", "\n", "The approach to directly search or download Argo data is to use `reg_a.datasets['argo'].search_data()`, and `reg_a.datasets['argo'].download()`. In both cases, the existing parameters and pressure ranges are used unless the user passes new `params` and/or `presRange` kwargs, respectively, which will directly update those values (stored attributes)." ] @@ -271,7 +266,7 @@ "source": [ "Now we can access the data for both Argo and ICESat-2! The below function will do this for us.\n", "\n", - "**Important**: The Argo data will be compiled into a Pandas DataFrame, which must be manually saved by the user. The ICESat-2 data is saved as processed HDF-5 files to the directory given below." + "**Important**: The Argo data will be compiled into a Pandas DataFrame, which must be manually saved by the user as demonstrated below. The ICESat-2 data is saved as processed HDF-5 files to the directory provided." ] }, { @@ -297,7 +292,7 @@ "user_expressions": [] }, "source": [ - "We now have two available Argo profiles, each containing `temperature` and `pressure`, compiled into a Pandas DataFrame. BGC Argo is also available through QUEST, so we could add more variables to this list.\n", + "We now have one available Argo profile, containing `temperature` and `pressure`, in a Pandas DataFrame. BGC Argo is also available through QUEST, so we could add more variables to this list.\n", "\n", "If the user wishes to add more profiles, parameters, and/or pressure ranges to a pre-existing DataFrame, then they should use `reg_a.datasets['argo'].download(keep_existing=True)` to retain previously downloaded data and have the new data added." ] @@ -309,9 +304,9 @@ "user_expressions": [] }, "source": [ - "The download function also provided a file containing ICESat-2 ATL03 data. Recall that because these data files are very large, we focus on only one file for this example.\n", + "The `reg_a.download_all()` function also provided a file containing ICESat-2 ATL03 data. Recall that because these data files are very large, we focus on only one file for this example.\n", "\n", - "The below workflow uses the icepyx Read module to quickly load ICESat-2 data into the XArray format. To read in multiple files, see the [icepyx Read tutorial](https://icepyx.readthedocs.io/en/latest/example_notebooks/IS2_data_read-in.html) for how to change your input source." + "The below workflow uses the icepyx Read module to quickly load ICESat-2 data into an Xarray DataSet. To read in multiple files, see the [icepyx Read tutorial](https://icepyx.readthedocs.io/en/latest/example_notebooks/IS2_data_read-in.html) for how to change your input source." ] }, { @@ -337,6 +332,7 @@ }, "outputs": [], "source": [ + "# decide which portions of the file to read in\n", "reader.vars.append(beam_list=['gt2l'], \n", " var_list=['h_ph', \"lat_ph\", \"lon_ph\", 'signal_conf_ph'])" ] @@ -361,7 +357,7 @@ "user_expressions": [] }, "source": [ - "To make the data more easily plottable, let's convert the data into a Pandas DataFrame. Note that this method is memory-intensive for ATL03 data, so users are suggested to look at small spatial domains to prevent the notebook from crashing. Here, since we only have data from one granule and ground track, we have sped up the conversion to a dataframe by first removing extra xarray dimensions we don't need for our plots. Several of the other steps completed below have analogous operations in xarray that would further reduce memory requirements and computation times." + "To make the data more easily plottable, let's convert the data into a Pandas DataFrame. Note that this method is memory-intensive for ATL03 data, so users are suggested to look at small spatial domains to prevent the notebook from crashing. Here, since we only have data from one granule and ground track, we have sped up the conversion to a dataframe by first removing extra data dimensions we don't need for our plots. Several of the other steps completed below using Pandas have analogous operations in Xarray that would further reduce memory requirements and computation times." ] }, { @@ -547,7 +543,6 @@ "argo_df.plot.scatter(ax=ax1, x='lon', y='lat', s=25.0, c='green', zorder=3, alpha=0.3)\n", "is2_pd_signal.plot.scatter(ax=ax1, x='lon_ph', y='lat_ph', s=10.0, zorder=2, alpha=0.3)\n", "ax1.plot(lons, lats, linewidth=1.5, color='orange', zorder=2)\n", - "#df.plot(ax=ax2, x='lon', y='lat', marker='o', color='red', markersize=2.5, zorder=3)\n", "ax1.set_xlim(-160,-100)\n", "ax1.set_ylim(20,50)\n", "ax1.set_aspect('equal', adjustable='box')\n", @@ -558,7 +553,6 @@ "argo_df.plot.scatter(ax=ax2, x='lon', y='lat', s=50.0, c='green', zorder=3, alpha=0.3)\n", "is2_pd_signal.plot.scatter(ax=ax2, x='lon_ph', y='lat_ph', s=10.0, zorder=2, alpha=0.3)\n", "ax2.plot(lons, lats, linewidth=1.5, color='orange', zorder=1)\n", - "ax2.scatter(-151.98956, 34.43885, color='orange', marker='^', s=80, zorder=4)\n", "ax2.set_xlim(min(lons) - lon_margin, max(lons) + lon_margin)\n", "ax2.set_ylim(min(lats) - lat_margin, max(lats) + lat_margin)\n", "ax2.set_aspect('equal', adjustable='box')\n", @@ -575,8 +569,8 @@ "ax3.set_yticklabels(['15', '10', '5', '0', '-5'])\n", "\n", "# Plot vertical ocean profile of the nearby Argo float\n", - "argo_df[argo_df['profile_id']=='4903409_053'].plot(ax=ax4, x='temperature', y='pressure', linewidth=3)\n", - "ax4.set_yscale('log')\n", + "argo_df.plot(ax=ax4, x='temperature', y='pressure', linewidth=3)\n", + "# ax4.set_yscale('log')\n", "ax4.invert_yaxis()\n", "ax4.get_legend().remove()\n", "ax4.set_xlabel('Temperature [$\\degree$C]', fontsize=18)\n", @@ -588,13 +582,24 @@ "#plt.savefig('/icepyx/quest/figures/is2_argo_figure.png', dpi=500)" ] }, + { + "cell_type": "markdown", + "id": "37720c79", + "metadata": {}, + "source": [ + "Recall that the Argo data must be saved manually.\n", + "The dataframe associated with the Quest object can be saved using `reg_a.save_all(path)`" + ] + }, { "cell_type": "code", "execution_count": null, "id": "9b6548e2-0662-4c8b-a251-55ca63aff99b", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "reg_a.save_all(path)" + ] } ], "metadata": { From ff3b4b62654c3e53c2b898328420ea7640864867 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 6 Dec 2023 22:28:36 +0000 Subject: [PATCH 124/124] GitHub action UML generation auto-update --- .../documentation/classes_dev_uml.svg | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/source/user_guide/documentation/classes_dev_uml.svg b/doc/source/user_guide/documentation/classes_dev_uml.svg index ba201f14a..765e0d531 100644 --- a/doc/source/user_guide/documentation/classes_dev_uml.svg +++ b/doc/source/user_guide/documentation/classes_dev_uml.svg @@ -30,20 +30,20 @@ icepyx.core.auth.EarthdataAuthMixin - -EarthdataAuthMixin - -_auth : NoneType -_s3_initial_ts : NoneType, datetime -_s3login_credentials : NoneType -_session : NoneType -auth -s3login_credentials -session - -__init__(auth) -__str__() -earthdata_login(uid, email, s3token): None + +EarthdataAuthMixin + +_auth : NoneType +_s3_initial_ts : NoneType, datetime +_s3login_credentials : NoneType +_session : NoneType +auth +s3login_credentials +session + +__init__(auth) +__str__() +earthdata_login(uid, email, s3token): None